Wagtail forms let the editor customize field types and titles, but not placeholder text. While you could write a custom template and hard-code the placeholder text, this quickly becomes a burden whenever the forms need changed. Here’s how to extend the Wagtail form field to enable editors to customize placeholder text.

First, we need to extend Wagtail’s field to hold our placeholder text:

from wagtail.contrib.forms.models import AbstractFormField

class CustomFormField(AbstractFormField):
    page = ParentalKey(
        "CustomFormPage",
        on_delete=models.CASCADE,
        related_name="custom_form_fields",
    )

    # add custom field to FormField model
    placeholder = models.CharField(
        "Placeholder",
        max_length=254,
        blank=True,
    )

    # enable our custom field in the admin UI
    panels = AbstractFormField.panels + [
        FieldPanel("placeholder"),
    ]

Note: this custom field is designed to not interfere with normal FormFields; if you want to apply this to all your form fields, replace the related name with related_name="form_fields"

Next, we need to extend Wagtail’s FormBuilder, to include our field in the render.

from wagtail.contrib.forms.forms import FormBuilder

class  CustomFormBuilder(FormBuilder):
    def get_create_field_function(self, type):
        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({
                "placeholder": field.placeholder,
            })
            return created_field
    return wrapped_create_field_function

Here we are wrapping the get_create_field_function to update our widgets attributes after making the field.

Finally, we need to tell our Form Page to use our custom builder and fields:

from wagtail.contrib.forms.models import AbstractEmailForm

class FormPage(AbstractEmailForm):
    # use custom form builder defined above
    form_builder = CustomFormBuilder

    body_content_panels = AbstractEmailForm.content_panels + [
        InlinePanel("custom_form_fields", label="Form fields"),
    ]
    def get_form_fields(self):
        return self.custom_form_fields.all()

Note that we override the get_form_fields function because we named our custom field something other than fields.

With this we’ve implemented a custom field that passes along its placeholder text to its widget to be displayed to our users.