James
James

Reputation: 461

Wagtail - adding CSS class or placeholder to form builder fields

I'm trying to add css classes (for column width) and placeholder text to my Wagtail form fields via the Wagtail admin form builder. I've tried using wagtail.contrib.forms and also the wagtailstreamforms package to no avail.

I know it says here that the Wagtail forms aren't a replacement for Django forms. However, without such basic functionality it's limited in its usefulness.

Upvotes: 1

Views: 1607

Answers (1)

LB Ben Johnston
LB Ben Johnston

Reputation: 5196

The below solution is a way to leverage the Wagtail contrib form builder to add a field to the UI where CMS users can add custom classes and a placeholder field.

Implementation

  • It is assumed you have a working FormPage similar to the implementation in the Wagtail form builder docs.
  • Firstly, you will need to add a new field to the FormField model, in the example below I have called this field_classname, remember to run Django migrations to update your DB.
  • To ensure that the field shows up in the admin UI you will need to modify the panels, this is similar to modifying the panels in a Page model.
  • At this point you should be able to open the Admin UI for a FormPage and see the new field and be able to save values to it.
  • Next step is to add a custom FormBuilder class that extends the one that is normally used and then on your FormPage model set this as an attribute via form_builder, this is similar to how new fields can be added as described in the docs.
  • This CustomFormBuilder will override the method get_create_field_function with a wrapper function that will return the generated field (which will be a Django Field instance).
  • Each Field instance will be returned almost the same, except for an update to the widget attrs which is essentially a Dict that you can add anything to.
  • IMPORTANT: Using the class attr will add the class name to the field but may not add it where you want, try this first though.
  • Assuming you have more granular control of your template rending, you will need to pass this attr to the template for each field div that is rendered, in the code below I have used the same field_classname key.
  • The critical change in the template is pulling out the widget attrs custom value set by our CustomFormBuilder -> <div class="fieldWrapper {{ field.field.widget.attrs.field_classname }}" aria-required={% if field.field.required %}"true"{% else %}"false"{% endif %}>

models.py

from django.db import models

from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.contrib.forms.forms import FormBuilder
from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField

# ... other imports


class FormField(AbstractFormField):

    page = ParentalKey("FormPage", related_name="form_fields", on_delete=models.CASCADE)
    
    # add custom fields to FormField model
    field_classname = models.CharField("Field classes", max_length=254, blank=True)
    placeholder = models.CharField("Placeholder", max_length=254, blank=True)
    
    # revise panels so that the field can be edited in the admin UI
    panels = AbstractFormField.panels + [
        FieldPanel("field_classname"),
        FieldPanel("placeholder"),
    ]


class CustomFormBuilder(FormBuilder):
    def get_create_field_function(self, type):
        """
        Override the method to prepare a wrapped function that will call the original
        function (which returns a field) and update the widget's attrs with a custom
        value that can be used within the template when rendering each field.
        """

        create_field_function = super().get_create_field_function(type)

        def wrapped_create_field_function(field, options):

            created_field = create_field_function(field, options)
            created_field.widget.attrs.update(
                # {"class": field.field_classname} # Important: using the class may be sufficient, depending on how your form is being rendered, try this first.
                {"field_classname": field.field_classname} # this is a non-standard attribute and will require custom template rendering of your form to work
                {"placeholder": field.placeholder},
            )

            return created_field

        return wrapped_create_field_function


class FormPage(AbstractEmailForm):
    form_builder = CustomFormBuilder  # use custom form builder to override behaviour

    # ... other form page fields, panels etc

templates/form_page.html

{% comment %}
You could render your form using a Django rendering shortcut such as `{{ form.as_p }}` but that will tend towards unsemantic code, and make it difficult to style. You can read more on Django form at:
https://docs.djangoproject.com/en/1.10/topics/forms/#form-rendering-options
{% endcomment %}
<form action="{% pageurl page %}" method="POST" role="form">
    {% csrf_token %}
    {% if form.subject.errors %}
        <ol role="alertdialog">
        {% for error in form.subject.errors %}
            <li role="alert"><strong>{{ error|escape }}</strong></li>
        {% endfor %}
        </ol>
    {% endif %}

    {% for field in form %}
        <div class="fieldWrapper {{ field.field.widget.attrs.field_classname }}" aria-required={% if field.field.required %}"true"{% else %}"false"{% endif %}>
            
            {{ field.label_tag }}{% if field.field.required %}<span class="required">*</span>{% endif %}

            {{ field }}
            
            {% if field.help_text %}
                <p class="help">{{ field.help_text|safe }}</p>
            {% endif %}
        </div>
    {% endfor %}

    <input type="submit">
</form>

Upvotes: 4

Related Questions