Skip to content

Commit b2e7174

Browse files
authored
Big form widgets refactor to allow user-defined widgets, added number widgets (#991)
* Big form widgets refactor to allow user-defined widgets, added number widgets * Deal with SQLCustomType in widgets.matches and WidgetRegistry * Don't always skip blob fields, allows users to define a custom blob widget (for all blobs or specific fields) * docs, fix compat with older widgets, remove shortcut type_name dict matching to allow proper overwriting * fix dumb oversight
1 parent 1264979 commit b2e7174

File tree

2 files changed

+641
-299
lines changed

2 files changed

+641
-299
lines changed

docs/chapter-12.rst

Lines changed: 124 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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
208208
with the ``requires`` parameter. We'll fully explain them
209209
on the :ref:`Form validation` paragraph.
210210

@@ -240,7 +240,7 @@ like to experiment, the database content can be fully seen and changed with the
240240
You can turn a create form into a CRUD update form by passing a record or a record id
241241
it 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
300300
Standard 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+
339349
Notice the differences from the 'Basic Form example' we've seen at the
340350
beginning 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
350361
dropdown menu!
351362

363+
352364
Using 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
359371
Custom 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

414493
In py4web a form is rendered by YATL helpers. This means the tree structure of a form
415494
can 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+
424508
Notice that a form does not make an HTML tree until form structure is accessed. Once accessed you can use ``.find(...)``
425509
to find matching elements. The argument of ``find`` is a string following the filter syntax of jQuery. In the above case
426510
there is a single match ``[0]`` and we modify the ``_class`` attribute of that element. Attribute names of HTML elements

0 commit comments

Comments
 (0)