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")} + + + + 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 name="render_vue_template_form()"> - % if form is not Undefined: - ${form.render_vue_template()} - % endif - - <%def name="render_vue_templates()"> ${parent.render_vue_templates()} ${self.render_vue_template_form()} +<%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 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()}