Skip to content

Commit 1decd41

Browse files
authored
Reapply "Big form widgets refactor to allow user-defined widgets, added number widgets (#991)" (#1001)
* Reapply "Big form widgets refactor to allow user-defined widgets, added number widgets (#991)" This reverts commit a1f6238. * fix: add forgotten instantiation of new-style widgets * Clarify in docs that FormStyleDefault should be cloned before modifying * add tests for FormStyle.widgets backwards compat, and fix discovered issue
1 parent 5016ac6 commit 1decd41

File tree

3 files changed

+669
-301
lines changed

3 files changed

+669
-301
lines changed

docs/chapter-12.rst

Lines changed: 127 additions & 42 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,51 +300,64 @@ 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()
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+
339351
Notice the differences from the 'Basic Form example' we've seen at the
340352
beginning 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
350363
dropdown menu!
@@ -359,51 +372,118 @@ Using widgets in forms is quite easy, and they'll let you have more control on i
359372
Custom 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

414494
In py4web a form is rendered by YATL helpers. This means the tree structure of a form
415495
can 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+
424509
Notice that a form does not make an HTML tree until form structure is accessed. Once accessed you can use ``.find(...)``
425510
to find matching elements. The argument of ``find`` is a string following the filter syntax of jQuery. In the above case
426511
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)