@@ -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,51 +300,64 @@ 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()
335- form = Form(db.thing, formstyle = FormStyleDefault)
340+ MyStyle = FormStyleDefault.clone()
341+ MyStyle.widgets[' color' ] = RadioWidget
342+ form = Form(db.thing, formstyle = MyStyle)
336343 rows = db(db.thing).select()
337344 return dict (form = form, rows = rows)
338345
346+ .. note ::
347+ The way Widgets work was changed in a recent update. For backwards compatibility, you can still pass a
348+ instance of a older style implicit widget, but for built-in widgets and Widget subclasses,
349+ you need to pass pass the Widget class without instantiating it. ``RadioWidget `` instead of ``RadioWidget() ``.
350+
339351Notice the differences from the 'Basic Form example' we've seen at the
340352beginning of the chapter:
341353
342354- 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:
355+ - before the form definition, you set the widgets dictionary entry
356+ corresponding to your field name to the desired Widget
344357
345358 .. code :: python
346-
347- FormStyleDefault .widgets[' color' ]= RadioWidget()
359+ MyStyle = FormStyleDefault.clone()
360+ MyStyle .widgets[' color' ] = RadioWidget
348361
349362 The result is the same as before, but now we have a radio button widget instead of the
350363dropdown menu!
@@ -359,51 +372,118 @@ Using widgets in forms is quite easy, and they'll let you have more control on i
359372Custom widgets
360373~~~~~~~~~~~~~~
361374
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:
375+ You can also customize the widgets properties by implementing custom widgets.
376+
377+ There are broadly 2 options to make ``Form `` use custom widgets:
378+
379+ - per-Field widgets, as shown above. Gives you more control, but has to be set for each Field/column individually.
380+ - Registered widgets with a matching method. Allows global matching on any characteristic of a Field.
381+
382+ When creating a custom widget, be aware of the methods you can and should overwrite:
383+
384+ - ``make_editable `` is for normal form inputs, this should be an input the user can change
385+ - ``make_readonly `` is for readonly displays of this field, for example when ``field.writable = False ``
386+ - ``make `` gets the value and calls the 2 above. Generally, you should prefer overwriting the 2 above
387+ - ``form_html `` calls ``make `` and generates the final HTML to be inserted into the form. It handles the HTML
388+ surrounding the bare form inputs, labels, field comment display, etc.
389+
390+
391+ Custom per-Field Widget
392+ """""""""""""""""""""""
364393
365394.. code :: python
366395
367396 # in controllers.py
368397 from py4web import action, redirect, URL , Field
369- from py4web.utils.form import Form, FormStyleDefault, RadioWidget
370- from pydal.validators import *
398+ from py4web.utils.form import Form, FormStyleDefault, Widget, RadioWidget, to_id
371399 from .common import db
372400
373401 # 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(
402+ class MyCustomWidget (Widget ):
403+ def make_editable (self , value ):
404+ return INPUT(
378405 _type = " text" ,
379- _id = " %s _ %s " % (tablename, field.name ),
380- _name = field.name,
406+ _id = to_id( self .field ),
407+ _name = self . field.name,
381408 _value = value,
382409 _class = " input" ,
383- _placeholder = placeholder if placeholder and placeholder != " " else " .. " ,
384- _title = title,
410+ _placeholder = self .placeholder ,
411+ _title = self . title,
385412 _style = " font-size: x-large;color: red; background-color: black;" ,
386413 )
387- return control
388-
414+
415+ # optionally overwrite the default readonly style
416+ # def make_readonly(self, value):
417+ # return DIV(str(value))
418+
389419 # controllers definition
390420 @action (" create_form" , method = [" GET" , " POST" ])
391421 @action.uses (" form_custom_widgets.html" , db)
392422 def create_form ():
393423 MyStyle = FormStyleDefault.clone()
394- MyStyle.classes = FormStyleDefault.classes
395- MyStyle.widgets[' name' ]= MyCustomWidget()
396- MyStyle.widgets[' color' ]= RadioWidget()
424+
425+ MyStyle.widgets[' name' ] = MyCustomWidget
426+ MyStyle.widgets[' color' ] = RadioWidget
397427
398428 form = Form(db.thing, deletable = False , formstyle = MyStyle)
399429 rows = db(db.thing).select()
400430 return dict (form = form, rows = rows)
401431
402432
403433 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,
434+ with foreground color red and background color black.
435+
436+ Registered Widget
437+ """""""""""""""""
438+ A registered Widget is globally registered to the widget registry at ``py4web.utils.form.widgets ``.
439+ This is how default widgets work, and allows you to overwrite default widgets or defines custom ones
440+ which apply to any matching field automatically.
441+
442+ To do this, a ``matches `` classmethod is used, which is checked when generating a form to determine
443+ the correct widget for a Field.
444+
445+ The most basic version just checks against the field type.
446+
447+ Note that matching occurs in reversed order of registration, which means Widgets defined (and imported)
448+ later will get checked first. This is what allows you to overwrite default fields, as those are
449+ always defined first.
450+
451+ In this example we will style all "string" fields which start with "n".
452+ We'll also inherit from the default TextInputWidget and only change its style and ``matches ``.
453+
454+ .. code :: python
455+
456+ # in controllers.py
457+ from py4web import action, redirect, URL , Field
458+ from py4web.utils.form import Form, FormStyleDefault, TextInputWidget, widgets
459+ from .common import db
460+
461+ # custom widget class definition
462+ @widgets.register_widget
463+ class MyCustomWidget (TextInputWidget ):
464+
465+ @ classmethod
466+ def matches (cls , field : Field) -> bool :
467+ return str (field.type) == " string" and field.name.startswith(" n" )
468+
469+ # since we don't need access to the value or structure
470+ # we can style the element whether its readonly or not
471+ def make (self , readonly : bool = False ):
472+ elem = super ().make(readonly)
473+ elem._style = " font-size: x-large; color: red; background-color: black;"
474+ return elem
475+
476+
477+ # the controller doesn't need to do anything special
478+ # since the Widget is registered
479+ @action (" create_form" , method = [" GET" , " POST" ])
480+ @action.uses (" form_custom_widgets.html" , db)
481+ def create_form ():
482+ form = Form(db.thing, deletable = False )
483+ rows = db(db.thing).select()
484+ return dict (form = form, rows = rows)
485+
405486
406- Even the radio button widget has changed, from red to blue.
407487
408488 Advanced form design
409489--------------------
@@ -413,14 +493,19 @@ Form structure manipulation
413493
414494In py4web a form is rendered by YATL helpers. This means the tree structure of a form
415495can be manipulated before the form is serialized in HTML.
416- Here is an example of how to manipulate the generate HTML structure:
496+ Here is an example of how to manipulate the generated HTML structure:
417497
418498.. code :: python
419499
420500 db.define_table(' paint' , Field(' color' ))
421501 form = Form(db.paint)
422502 form.structure.find(' [name=color]' )[0 ][' _class' ] = ' my-class'
423503
504+ .. note ::
505+
506+ For demonstration purposes. For changes like this, you should consider
507+ adjusting the FormStyle or using a custom Widget instead.
508+
424509Notice that a form does not make an HTML tree until form structure is accessed. Once accessed you can use ``.find(...) ``
425510to find matching elements. The argument of ``find `` is a string following the filter syntax of jQuery. In the above case
426511there is a single match ``[0] `` and we modify the ``_class `` attribute of that element. Attribute names of HTML elements
0 commit comments