diff --git a/src/wuttaweb/forms/base.py b/src/wuttaweb/forms/base.py
index 6365aa8..0d2a42d 100644
--- a/src/wuttaweb/forms/base.py
+++ b/src/wuttaweb/forms/base.py
@@ -275,6 +275,10 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
deform_form = None
validated = None
+ vue_template = "/forms/vue_template.mako"
+ fields_template = "/forms/vue_fields.mako"
+ buttons_template = "/forms/vue_buttons.mako"
+
def __init__( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
self,
request,
@@ -331,6 +335,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
self.show_button_cancel = show_button_cancel
self.button_label_cancel = button_label_cancel
self.auto_disable_cancel = auto_disable_cancel
+ self.form_attrs = {}
self.config = self.request.wutta_config
self.app = self.config.get_app()
@@ -806,7 +811,10 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# get fields
fields = self.get_fields()
if not fields:
- raise NotImplementedError
+ raise ValueError(
+ "could not determine fields list; "
+ "please set model_class or fields explicitly"
+ )
if self.model_class:
@@ -939,7 +947,7 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
"""
return HTML.tag(self.vue_tagname, **kwargs)
- def render_vue_template(self, template="/forms/vue_template.mako", **context):
+ def render_vue_template(self, template=None, **context):
"""
Render the Vue template block for the form.
@@ -954,8 +962,8 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
@@ -969,36 +977,121 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
Actual output will of course depend on form attributes, i.e.
:attr:`vue_tagname` and :attr:`fields` list etc.
- :param template: Path to Mako template which is used to render
- the output.
+ Default logic will also invoke (indirectly):
+
+ * :meth:`render_vue_fields()`
+ * :meth:`render_vue_buttons()`
+
+ :param template: Optional template path to override the class
+ default.
+
+ :returns: HTML literal
"""
+ context = self.get_vue_context(**context)
+ html = render(template or self.vue_template, context)
+ return HTML.literal(html)
+
+ def get_vue_context(self, **context): # pylint: disable=missing-function-docstring
context["form"] = self
context["dform"] = self.get_deform()
context.setdefault("request", self.request)
context["model_data"] = self.get_vue_model_data()
# set form method, enctype
- context.setdefault("form_attrs", {})
- context["form_attrs"].setdefault("method", self.action_method)
+ form_attrs = context.setdefault("form_attrs", dict(self.form_attrs))
+ form_attrs.setdefault("method", self.action_method)
if self.action_method == "post":
- context["form_attrs"].setdefault("enctype", "multipart/form-data")
+ form_attrs.setdefault("enctype", "multipart/form-data")
# auto disable button on submit
if self.auto_disable_submit:
- context["form_attrs"]["@submit"] = "formSubmitting = true"
+ form_attrs["@submit"] = "formSubmitting = true"
- output = render(template, context)
- return HTML.literal(output)
+ # duplicate entire context for sake of fields/buttons template
+ context["form_context"] = context
+
+ return context
+
+ def render_vue_fields(self, context, template=None, **kwargs):
+ """
+ Render the fields section within the form template.
+
+ This is normally invoked from within the form's
+ ``vue_template`` like this:
+
+ .. code-block:: none
+
+ ${form.render_vue_fields(form_context)}
+
+ There is a default ``fields_template`` but that is only the
+ last resort. Logic will first look for a
+ ``form_vue_fields()`` def within the *main template* being
+ rendered for the page.
+
+ An example will surely help:
+
+ .. code-block:: mako
+
+ <%inherit file="/master/edit.mako" />
+
+ <%def name="form_vue_fields()">
+
+
this is my custom fields section:
+
+ ${form.render_vue_field("myfield")}
+
+ %def>
+
+ This keeps the custom fields section within the main page
+ template as opposed to yet another file. But if your page
+ template has no ``form_vue_fields()`` def, then the class
+ default template is used. (Unless the ``template`` param
+ is specified.)
+
+ See also :meth:`render_vue_template()` and
+ :meth:`render_vue_buttons()`.
+
+ :param context: This must be the original context as provided
+ to the form's ``vue_template``. See example above.
+
+ :param template: Optional template path to use instead of the
+ defaults described above.
+
+ :returns: HTML literal
+ """
+ context.update(kwargs)
+ html = False
+
+ if not template:
+
+ if main_template := context.get("main_template"):
+ try:
+ vue_fields = main_template.get_def("form_vue_fields")
+ except AttributeError:
+ pass
+ else:
+ html = vue_fields.render(**context)
+
+ if html is False:
+ template = self.fields_template
+
+ if html is False:
+ html = render(template, context)
+
+ return HTML.literal(html)
def render_vue_field( # pylint: disable=unused-argument,too-many-locals
self,
fieldname,
readonly=None,
+ label=True,
+ horizontal=True,
**kwargs,
):
"""
Render the given field completely, i.e. ```` wrapper
- with label and containing a widget.
+ with label and a widget, with validation errors flagged as
+ needed.
Actual output will depend on the field attributes etc.
Typical output might look like:
@@ -1009,14 +1102,23 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
horizontal
type="is-danger"
message="something went wrong!">
-
+
- .. warning::
+ :param fieldname: Name of field to render.
- Any ``**kwargs`` received from caller are ignored by this
- method. For now they are allowed, for sake of backwawrd
- compatibility. This may change in the future.
+ :param readonly: Optional override for readonly flag.
+
+ :param label: Whether to include/set the field label.
+
+ :param horizontal: Boolean value for the ``horizontal`` flag
+ on the field.
+
+ :param \\**kwargs: Remaining kwargs are passed to widget's
+ ``serialize()`` method.
+
+ :returns: HTML literal
"""
# readonly comes from: caller, field flag, or form flag
if readonly is None:
@@ -1034,10 +1136,9 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
# render proper widget if field is in deform/schema
field = dform[fieldname]
- kw = {}
if readonly:
- kw["readonly"] = True
- html = field.serialize(**kw)
+ kwargs["readonly"] = True
+ html = field.serialize(**kwargs)
else:
# render static text if field not in deform/schema
@@ -1052,12 +1153,13 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
html = HTML.literal(html or " ")
# render field label
- label = self.get_label(fieldname)
+ if label:
+ label = self.get_label(fieldname)
# b-field attrs
attrs = {
- ":horizontal": "true",
- "label": label,
+ ":horizontal": "true" if horizontal else "false",
+ "label": label or "",
}
# next we will build array of messages to display..some
@@ -1085,6 +1187,36 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
return HTML.tag("b-field", c=[html], **attrs)
+ def render_vue_buttons(self, context, template=None, **kwargs):
+ """
+ Render the buttons section within the form template.
+
+ This is normally invoked from within the form's
+ ``vue_template`` like this:
+
+ .. code-block:: none
+
+ ${form.render_vue_buttons(form_context)}
+
+ .. note::
+
+ This method does not yet inspect the main page template,
+ unlike :meth:`render_vue_fields()`.
+
+ See also :meth:`render_vue_template()`.
+
+ :param context: This must be the original context as provided
+ to the form's ``vue_template``. See example above.
+
+ :param template: Optional template path to override the class
+ default.
+
+ :returns: HTML literal
+ """
+ context.update(kwargs)
+ html = render(template or self.buttons_template, context)
+ return HTML.literal(html)
+
def render_vue_finalize(self):
"""
Render the Vue "finalize" script for the form.
@@ -1103,6 +1235,25 @@ class Form: # pylint: disable=too-many-instance-attributes,too-many-public-meth
"""
return render_vue_finalize(self.vue_tagname, self.vue_component)
+ def get_field_vmodel(self, field):
+ """
+ Convenience to return the ``v-model`` data reference for the
+ given field. For instance:
+
+ .. code-block:: none
+
+
+
+
+ easter egg!
+
+
+ :returns: JS-valid string referencing the field value
+ """
+ dform = self.get_deform()
+ return f"modelData.{dform[field].oid}"
+
def get_vue_model_data(self):
"""
Returns a dict with form model data. Values may be nested
diff --git a/src/wuttaweb/templates/form.mako b/src/wuttaweb/templates/form.mako
index ef6a6a6..a5ba1f4 100644
--- a/src/wuttaweb/templates/form.mako
+++ b/src/wuttaweb/templates/form.mako
@@ -34,17 +34,19 @@
<%def name="tool_panels()">%def>
-<%def name="render_vue_template_form()">
- % if form is not Undefined:
- ${form.render_vue_template()}
- % endif
-%def>
-
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${self.render_vue_template_form()}
%def>
+<%def name="render_vue_template_form()">
+ % if form is not Undefined:
+ ## nb. must provide main template to form, so it can
+ ## do 'def' lookup as needed for fields template etc.
+ ${form.render_vue_template(main_template=self.template)}
+ % endif
+%def>
+
<%def name="make_vue_components()">
${parent.make_vue_components()}
% if form is not Undefined:
diff --git a/src/wuttaweb/templates/forms/vue_buttons.mako b/src/wuttaweb/templates/forms/vue_buttons.mako
new file mode 100644
index 0000000..2aeccd9
--- /dev/null
+++ b/src/wuttaweb/templates/forms/vue_buttons.mako
@@ -0,0 +1,43 @@
+## -*- coding: utf-8; -*-
+% if not form.readonly:
+
+
+
+ % if form.show_button_cancel:
+
+ % endif
+
+ % if form.show_button_reset:
+
+ Reset
+
+ % endif
+
+
+ % if form.auto_disable_submit:
+ {{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
+ % else:
+ ${form.button_label_submit}
+ % endif
+
+
+
+% endif
diff --git a/src/wuttaweb/templates/forms/vue_fields.mako b/src/wuttaweb/templates/forms/vue_fields.mako
new file mode 100644
index 0000000..22db918
--- /dev/null
+++ b/src/wuttaweb/templates/forms/vue_fields.mako
@@ -0,0 +1,4 @@
+## -*- coding: utf-8; -*-
+% for fieldname in form:
+ ${form.render_vue_field(fieldname, horizontal=True)}
+% endfor
diff --git a/src/wuttaweb/templates/forms/vue_template.mako b/src/wuttaweb/templates/forms/vue_template.mako
index facc89d..f754e7c 100644
--- a/src/wuttaweb/templates/forms/vue_template.mako
+++ b/src/wuttaweb/templates/forms/vue_template.mako
@@ -14,66 +14,21 @@
% endfor
% endif
-
- % for fieldname in form:
- ${form.render_vue_field(fieldname)}
- % endfor
-
+ ${form.render_vue_fields(form_context)}
- % if not form.readonly:
-
-
-
- % if form.show_button_cancel:
-
- % endif
-
- % if form.show_button_reset:
-
- Reset
-
- % endif
-
-
- % if form.auto_disable_submit:
- {{ formSubmitting ? "Working, please wait..." : "${form.button_label_submit}" }}
- % else:
- ${form.button_label_submit}
- % endif
-
-
-
- % endif
+ ${form.render_vue_buttons(form_context)}
${h.end_form()}