Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
dialogs module. Extending it....
-
@omz, I am not sure if you are interested in extending the dialogs module further or not.
But 2 easy things come to mind.. In the fields dict you could include a list for validation eg. ['Mr', 'Mrs', 'Dr'] etc... With a match case predicate.
The other one I think about is a list of allowed chars or the inverse of that. So if I had a disallowed list like [" "], would stop the user from entering a space in the field. This would give a one word field. Of course would not stop punctuation etc, but you could also add constants to filter out combinations also.Another way and I think might be better is to just add a callback function to the list of types.
Or callback functions. Like validate, keypress, leaving field etc... Ok, then just delegates I guess. I haven't worked with the text delegates yet.The extra mile might be even to have a callback to create the cell and assign a delegate to it.
I am not sure about your idea with the dialogs. I am trying to write a DialogManager around it. If it stays the same, it's still good. But with a few enhancements I think it could be great
-
Yeah, some sort of data validation for
dialogs.form_dialog()
would definitely be useful.Here's an idea for a relatively simple implementation that wouldn't require changes in the
dialogs
module itself (though it duplicates some code from there). It's not as granular as you might want (e.g. it's not possible to validate input while typing), but it's probably good enough for a lot of use cases. It's basically just an additionalvalidator
parameter, which should be a function/callable. When the Done button in the dialog is tapped, this function gets the (preliminary) result of the dialog, can inspect it, and it should return a list of fields (keys) that are invalid for whatever reason. These fields are then highlighted in red, and the dialog remains on screen.# Extension of dialogs.form_dialog() that supports simple data validation import dialogs import collections import ui class ValidatingFormDialogController (dialogs._FormDialogController): def done_action(self, sender): if callable(self.validator): invalid_keys = self.validator(self.values) if invalid_keys: for i, (title, fields) in enumerate(self.sections): for j, field in enumerate(fields): cell = self.cells[i][j] if field['key'] in invalid_keys: cell.text_label.text_color = 'red' else: cell.text_label.text_color = None return if self.shield_view: self.dismiss_datepicker(None) else: ui.end_editing() self.was_canceled = False self.container_view.close() def form_dialog(title='', fields=None, sections=None, done_button_title='Done', validator=None): if not sections and not fields: raise ValueError('sections or fields are required') if not sections: sections = [('', fields)] if not isinstance(title, str) and not isinstance(title, unicode): raise TypeError('title must be a string') for section in sections: if not isinstance(section, collections.Sequence): raise TypeError('Sections must be sequences (title, fields)') if len(section) < 2: raise TypeError('Sections must have 2 or 3 items (title, fields[, footer]') if not isinstance(section[0], str) and not isinstance(section[0], unicode): raise TypeError('Section titles must be strings') if not isinstance(section[1], collections.Sequence): raise TypeError('Expected a sequence of field dicts') for field in section[1]: if not isinstance(field, dict): raise TypeError('fields must be dicts') c = ValidatingFormDialogController(title, sections, done_button_title=done_button_title) c.validator = validator c.container_view.present('sheet') c.container_view.wait_modal() c.container_view = None if c.was_canceled: return None return c.values # --- DEMO: def validate_form(values): # This gets called before the dialog is dismissed via 'Done'. # It should return a list of keys that failed to pass validation. # If no invalid keys are returned, the dialog is closed as usual, # otherwise, the invalid fields are highlighted in red. invalid = [] # Title must be 'Mr', 'Mrs', or 'Ms': if values.get('title') not in ['Mr', 'Mrs', 'Ms']: invalid.append('title') # Name must not be empty: if len(values.get('name')) < 1: invalid.append('name') # 'Accept Terms' must be checked: if not values.get('terms'): invalid.append('terms') return invalid def main(): fields = [{'type': 'text', 'key': 'title', 'title': 'Title (Mr/Mrs/Ms)'}, {'type': 'text', 'key': 'name', 'title': 'Name'}, {'type': 'switch', 'key': 'terms', 'title': 'Accept Terms'}] r = form_dialog('Test', fields, validator=validate_form) print r if __name__ == '__main__': main()
-
@omz, I am a little out of my depth here. But what I think is that with this approach you have ended up opening the whole of the dialogs module or most of it.
Eg. I can access all the TableViewCells and get to the TextFields from the cells subviews etc. Theoretically allowing me to change the the delegate of the TextField, add items to the cell etc...
Add another right_buttons_items and so...But before I go running off down this path, I wanted to check in with you. maybe my thinking is too simplistic and flawed. maybe there are some gotchas I am not getting.
I did some simple tests, but basic. Just to make sure I could access what I thought I could.
-
@omz ok, here is a super crude update.
# coding: utf-8 # Extension of dialogs.form_dialog() that supports simple data validation import dialogs import collections import ui class ValidatingFormDialogController (dialogs._FormDialogController): def done_action(self, sender): if callable(self.validator): invalid_keys = self.validator(self.values) if invalid_keys: for i, (title, fields) in enumerate(self.sections): for j, field in enumerate(fields): cell = self.cells[i][j] if field['key'] in invalid_keys: cell.text_label.text_color = 'red' else: cell.text_label.text_color = None return if self.shield_view: self.dismiss_datepicker(None) else: ui.end_editing() self.was_canceled = False self.container_view.close() class MyTextFieldDelegate (object): def textfield_should_begin_editing(self, textfield): return True def textfield_did_begin_editing(self, textfield): pass def textfield_did_end_editing(self, textfield): pass def textfield_should_return(self, textfield): textfield.end_editing() return True def textfield_should_change(self, textfield, range, replacement): if replacement == ' ': return False print textfield, range, replacement return True def textfield_did_change(self, textfield): pass def form_dialog(title='', fields=None, sections=None, done_button_title='Done', validator=None): if not sections and not fields: raise ValueError('sections or fields are required') if not sections: sections = [('', fields)] if not isinstance(title, str) and not isinstance(title, unicode): raise TypeError('title must be a string') for section in sections: if not isinstance(section, collections.Sequence): raise TypeError('Sections must be sequences (title, fields)') if len(section) < 2: raise TypeError('Sections must have 2 or 3 items (title, fields[, footer]') if not isinstance(section[0], str) and not isinstance(section[0], unicode): raise TypeError('Section titles must be strings') if not isinstance(section[1], collections.Sequence): raise TypeError('Expected a sequence of field dicts') for field in section[1]: if not isinstance(field, dict): raise TypeError('fields must be dicts') c = ValidatingFormDialogController(title, sections, done_button_title=done_button_title) cell = c.cells[0][0] print c.cells[0][0].text_label.text tf = cell.content_view.subviews[0] tf.text = 'Hi there' tf.clear_button_mode = 'when_editing' tf.delegate = MyTextFieldDelegate() c.validator = validator c.container_view.present('sheet') c.container_view.wait_modal() c.container_view = None if c.was_canceled: return None return c.values # --- DEMO: def validate_form(values): # This gets called before the dialog is dismissed via 'Done'. # It should return a list of keys that failed to pass validation. # If no invalid keys are returned, the dialog is closed as usual, # otherwise, the invalid fields are highlighted in red. invalid = [] # Title must be 'Mr', 'Mrs', or 'Ms': if values.get('title') not in ['Mr', 'Mrs', 'Ms']: invalid.append('title') # Name must not be empty: if len(values.get('name')) < 1: invalid.append('name') # 'Accept Terms' must be checked: if not values.get('terms'): invalid.append('terms') return invalid def main(): fields = [{'type': 'text', 'key': 'title', 'title': 'Title (Mr/Mrs/Ms)'}, {'type': 'text', 'key': 'name', 'title': 'Name'}, {'type': 'switch', 'key': 'terms', 'title': 'Accept Terms'}] r = form_dialog('Test', fields, validator=validate_form) print r if __name__ == '__main__': main()```
-
As far as I can see, the validation can happen in the delegate, but it's exciting
-
@omz , no need to waste time answering me. I have done quite a few tests and it works fine. Of course have to be mindful changing text sizes etc. if this is required could copy the code you use to size and position the edit field into a resize function anyway.
I noticed you are using ui.measure_string. I assume you have fixed this as I remember it was broken in 1.5
It would be nice if in your next release you could name the TextField used for input. Just would give more certainty when retrieving it.But I have to say, what you did is very nice. From a closed system, to a very flexible one. Also very easy to add menu btns with actions etc...
Edit/addition
Oh, I will also try emulating your date picker to make custom slide panels. Not sure what to use them for yet. But I am sure, they could be usefulAnyway, thanks for sharing it....
-
@omz, Is coming along. The view in the pic is a pyui file. Loaded at runtime and with the exact same animations you do for datepicker. Dismisses also correctly as you do with date picker.
For the moment, the cancel btn in the menu activates it. Cl8cking the shield dismisses it. Not returning values yet.What I have done so far is very tied into the dialog. I made a class for the slide up panel, but I pass it a reference to the dialog. The easy way first. I am sure I can uncouple it.
But, more my point is that this would a great addition to the ui. Look, if I could write it professionally I would. I will write a seperate class sometime for ui, but will still be amateur hour for me. -
Hello @Phuket2 , I found your topic by searching validation of a form dialog.
I need something similar and I tried the delegate method of the textfield.
Checking while typing is ok (in textfield_should_change) but the form_dialog does not return the changed values.
Did you success with this code?If I put these two lines after wait_modal,
c.container_view.wait_modal() print('tf.text=',tf.text) print('c.values=',c.values)
tf.text contains the typed value, but c.values not!?
Solved (for easy case of only one section) by:
for i in range(0,len(c.cells[0])): # loop on rows of section 0 cell = c.cells[0][i] # ui.TableViewCell of row i tf = cell.content_view.subviews[0] # ui.TextField of value in row tf.delegate = MyTextFieldDelegate() c.validator = validator c.container_view.present('sheet') c.container_view.wait_modal() # Get rid of the view to avoid a retain cycle: c.container_view = None if c.was_canceled: return None for i in range(0,len(c.cells[0])): # loop on rows of section 0 cell = c.cells[0][i] # ui.TableViewCell of row i tf = cell.content_view.subviews[0] # ui.TextField of value in row c.values[tf.name] = tf.text # set return values print('c.values=',c.values) return c.values
-
@cvp , sorry I did not get back to you. I have been inundated with visitors. I would not have that been that much help anyway, was so -long ago. But great you solved it.