@@ -204,7 +204,7 @@ Create a new minimal app called ``form_basic`` :
204204 return dict (form = form, rows = rows)
205205
206206
207- Note the import of two simple validators on top, in order to be used later
207+ Note the import of validators at the top. This will be used later
208208with the ``requires `` parameter. We'll fully explain them
209209on the :ref: `Form validation ` paragraph.
210210
@@ -240,7 +240,7 @@ like to experiment, the database content can be fully seen and changed with the
240240You can turn a create form into a CRUD update form by passing a record or a record id
241241it second argument:
242242
243- .. code :: html
243+ .. code :: python
244244
245245 # controllers definition
246246 @action (" update_form/<thing_id:int>" , method = [" GET" , " POST" ])
@@ -300,55 +300,67 @@ Widgets
300300Standard widgets
301301~~~~~~~~~~~~~~~~
302302
303- Py4web provides many widgets in the py4web.utility.form library. They are simple plugins
304- that easily allow you to specify the type of the input elements in a form, along with
305- some of their properties.
306-
307- Here is the full list:
303+ Py4web provides many widgets in the py4web.utility.form library. They are used by ``Form `` to generate
304+ the HTML of form fields. All widgets inherit from the ``Widget `` Abstract Base Class, and should be
305+ registered to the ``widgets `` registry object.
308306
309- - CheckboxWidget
310- - DateTimeWidget
311- - FileUploadWidget
312- - ListWidget
313- - PasswordWidget
314- - RadioWidget
315- - SelectWidget
316- - TextareaWidget
307+ Here is the full list of the pydal types and their widgets:
317308
309+ - ``string ``: TextInputWidget
310+ - ``date ``: DateInputWidget
311+ - ``time ``: TimeInputWidget
312+ - ``integer ``: IntegerInputWidget
313+ - ``numeric ``: FloatInputWidget
314+ - ``datetime ``: DateTimeWidget
315+ - ``text ``: TextareaWidget
316+ - ``json ``: JsonWidget
317+ - ``boolean ``: CheckboxWidget
318+ - ``list ``:: ListWidget
319+ - ``password ``: PasswordWidget
320+ - ``select ``: SelectWidget
321+ - ``radio ``: RadioWidget
322+ - ``upload ``: FileUploadWidget
323+ - ``blob ``: BlobWidget - no-op widget, can be overwritten but does nothing by default
318324
319- This is an improved 'Basic Form Example' with a radio button widget:
320325
326+ By default Widgets are chosen based on DAL Field type. You can also use choose widgets for individual fields,
327+ like in this improved 'Basic Form Example' with a radio button widget:
321328
322329.. code :: python
323330
324331 # in controllers.py
325332 from py4web import action, redirect, URL , Field
326333 from py4web.utils.form import Form, FormStyleDefault, RadioWidget
327- from pydal.validators import *
328334 from .common import db
329335
330336 # controllers definition
331337 @action (" create_form" , method = [" GET" , " POST" ])
332338 @action.uses (" form_widgets.html" , db)
333339 def create_form ():
334- FormStyleDefault.widgets[' color' ]= RadioWidget()
340+ FormStyleDefault.widgets[' color' ] = RadioWidget
335341 form = Form(db.thing, formstyle = FormStyleDefault)
336342 rows = db(db.thing).select()
337343 return dict (form = form, rows = rows)
338344
345+ .. note ::
346+ The way Widgets work was changed in a recent update. You used to pass a instance of a Widget
347+ but now you pass the Widget class. ``RadioWidget `` instead of ``RadioWidget() ``.
348+
339349Notice the differences from the 'Basic Form example' we've seen at the
340350beginning of the chapter:
341351
342352- you need to import the widget from the py4web.utils.form library
343- - before the form definition, you define the ``color `` field form style with the line:
353+ - before the form definition, you set the widgets dictionary entry
354+ corresponding to your field name to the desired Widget
344355
345356 .. code :: python
346357
347- FormStyleDefault.widgets[' color' ]= RadioWidget()
358+ FormStyleDefault.widgets[' color' ] = RadioWidget
348359
349360 The result is the same as before, but now we have a radio button widget instead of the
350361dropdown menu!
351362
363+
352364Using widgets in forms is quite easy, and they'll let you have more control on its pieces.
353365
354366.. important ::
@@ -359,51 +371,118 @@ Using widgets in forms is quite easy, and they'll let you have more control on i
359371Custom widgets
360372~~~~~~~~~~~~~~
361373
362- You can also customize the widgets properties by cloning and modifying and existing style.
363- Let's have a quick look, improving again our Superhero example:
374+ You can also customize the widgets properties by implementing custom widgets.
375+
376+ There are broadly 2 options to make ``Form `` use custom widgets:
377+
378+ - per-Field widgets, as shown above. Gives you more control, but has to be set for each Field/column individually.
379+ - Registered widgets with a matching method. Allows global matching on any characteristic of a Field.
380+
381+ When creating a custom widget, be aware of the methods you can and should overwrite:
382+
383+ - ``make_editable `` is for normal form inputs, this should be an input the user can change
384+ - ``make_readonly `` is for readonly displays of this field, for example when ``field.writable = False ``
385+ - ``make `` gets the value and calls the 2 above. Generally, you should prefer overwriting the 2 above
386+ - ``form_html `` calls ``make `` and generates the final HTML to be inserted into the form. It handles the HTML
387+ surrounding the bare form inputs, labels, field comment display, etc.
388+
389+
390+ Custom per-Field Widget
391+ """""""""""""""""""""""
364392
365393.. code :: python
366394
367395 # in controllers.py
368396 from py4web import action, redirect, URL , Field
369- from py4web.utils.form import Form, FormStyleDefault, RadioWidget
370- from pydal.validators import *
397+ from py4web.utils.form import Form, FormStyleDefault, Widget, RadioWidget, to_id
371398 from .common import db
372399
373400 # custom widget class definition
374- class MyCustomWidget :
375- def make (self , field , value , error , title , placeholder , readonly = False ):
376- tablename = field._table if " _table" in dir (field) else " no_table"
377- control = INPUT(
401+ class MyCustomWidget (Widget ):
402+ def make_editable (self , value ):
403+ return INPUT(
378404 _type = " text" ,
379- _id = " %s _ %s " % (tablename, field.name ),
380- _name = field.name,
405+ _id = to_id( self .field ),
406+ _name = self . field.name,
381407 _value = value,
382408 _class = " input" ,
383- _placeholder = placeholder if placeholder and placeholder != " " else " .. " ,
384- _title = title,
409+ _placeholder = self .placeholder ,
410+ _title = self . title,
385411 _style = " font-size: x-large;color: red; background-color: black;" ,
386412 )
387- return control
388-
413+
414+ # optionally overwrite the default readonly style
415+ # def make_readonly(self, value):
416+ # return DIV(str(value))
417+
389418 # controllers definition
390419 @action (" create_form" , method = [" GET" , " POST" ])
391420 @action.uses (" form_custom_widgets.html" , db)
392421 def create_form ():
393422 MyStyle = FormStyleDefault.clone()
394- MyStyle.classes = FormStyleDefault.classes
395- MyStyle.widgets[' name' ]= MyCustomWidget()
396- MyStyle.widgets[' color' ]= RadioWidget()
423+
424+ MyStyle.widgets[' name' ] = MyCustomWidget
425+ MyStyle.widgets[' color' ] = RadioWidget
397426
398427 form = Form(db.thing, deletable = False , formstyle = MyStyle)
399428 rows = db(db.thing).select()
400429 return dict (form = form, rows = rows)
401430
402431
403432 The result is similar to the previous ones, but now we have a custom input field,
404- with foreground color red and background color black,
433+ with foreground color red and background color black.
434+
435+ Registered Widget
436+ """""""""""""""""
437+ A registered Widget is globally registered to the widget registry at ``py4web.utils.form.widgets ``.
438+ This is how default widgets work, and allows you to overwrite default widgets or defines custom ones
439+ which apply to any matching field automatically.
440+
441+ To do this, a ``matches `` classmethod is used, which is checked when generating a form to determine
442+ the correct widget for a Field.
443+
444+ The most basic version just checks against the field type.
445+
446+ Note that matching occurs in reversed order of registration, which means Widgets defined (and imported)
447+ later will get checked first. This is what allows you to overwrite default fields, as those are
448+ always defined first.
449+
450+ In this example we will style all "string" fields which start with "n".
451+ We'll also inherit from the default TextInputWidget and only change its style and ``matches ``.
452+
453+ .. code :: python
454+
455+ # in controllers.py
456+ from py4web import action, redirect, URL , Field
457+ from py4web.utils.form import Form, FormStyleDefault, TextInputWidget, widgets
458+ from .common import db
459+
460+ # custom widget class definition
461+ @widgets.register_widget
462+ class MyCustomWidget (TextInputWidget ):
463+
464+ @ classmethod
465+ def matches (cls , field : Field) -> bool :
466+ return str (field.type) == " string" and field.name.startswith(" n" )
467+
468+ # since we don't need access to the value or structure
469+ # we can style the element whether its readonly or not
470+ def make (self , readonly : bool = False ):
471+ elem = super ().make(readonly)
472+ elem._style = " font-size: x-large; color: red; background-color: black;"
473+ return elem
474+
475+
476+ # the controller doesn't need to do anything special
477+ # since the Widget is registered
478+ @action (" create_form" , method = [" GET" , " POST" ])
479+ @action.uses (" form_custom_widgets.html" , db)
480+ def create_form ():
481+ form = Form(db.thing, deletable = False )
482+ rows = db(db.thing).select()
483+ return dict (form = form, rows = rows)
484+
405485
406- Even the radio button widget has changed, from red to blue.
407486
408487 Advanced form design
409488--------------------
@@ -413,14 +492,19 @@ Form structure manipulation
413492
414493In py4web a form is rendered by YATL helpers. This means the tree structure of a form
415494can be manipulated before the form is serialized in HTML.
416- Here is an example of how to manipulate the generate HTML structure:
495+ Here is an example of how to manipulate the generated HTML structure:
417496
418497.. code :: python
419498
420499 db.define_table(' paint' , Field(' color' ))
421500 form = Form(db.paint)
422501 form.structure.find(' [name=color]' )[0 ][' _class' ] = ' my-class'
423502
503+ .. note ::
504+
505+ For demonstration purposes. For changes like this, you should consider
506+ adjusting the FormStyle or using a custom Widget instead.
507+
424508Notice that a form does not make an HTML tree until form structure is accessed. Once accessed you can use ``.find(...) ``
425509to find matching elements. The argument of ``find `` is a string following the filter syntax of jQuery. In the above case
426510there is a single match ``[0] `` and we modify the ``_class `` attribute of that element. Attribute names of HTML elements
0 commit comments