How to build custom StreamField blocks

Custom editing interfaces for StructBlock

To customize the styling of a StructBlock as it appears in the page editor, you can specify a form_classname attribute (either as a keyword argument to the StructBlock constructor, or in a subclass’s Meta) to override the default value of struct-block:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    class Meta:
        icon = 'user'
        form_classname = 'person-block struct-block'

You can then provide custom CSS for this block, targeted at the specified classname, by using the insert_global_admin_css hook.

Note

Wagtail’s editor styling has some built-in styling for the struct-block class and other related elements. If you specify a value for form_classname, it will overwrite the classes that are already applied to StructBlock, so you must remember to specify the struct-block as well.

For more extensive customizations that require changes to the HTML markup as well, you can override the form_template attribute in Meta to specify your own template path. The following variables are available on this template:

children
An OrderedDict of BoundBlocks for all of the child blocks making up this StructBlock.

help_text
The help text for this block, if specified.

classname The class name passed as form_classname (defaults to struct-block).

block_definition The StructBlock instance that defines this block.

prefix The prefix used on form fields for this block instance, guaranteed to be unique across the form.

To add additional variables, you can override the block’s get_form_context method:

class PersonBlock(blocks.StructBlock):
    first_name = blocks.CharBlock()
    surname = blocks.CharBlock()
    photo = ImageChooserBlock(required=False)
    biography = blocks.RichTextBlock()

    def get_form_context(self, value, prefix='', errors=None):
        context = super().get_form_context(value, prefix=prefix, errors=errors)
        context['suggested_first_names'] = ['John', 'Paul', 'George', 'Ringo']
        return context

    class Meta:
        icon = 'user'
        form_template = 'myapp/block_forms/person.html'

A form template for a StructBlock must include the output of render_form for each child block in the children dict, inside a container element with a data-contentpath attribute equal to the block’s name. This attribute is used by the commenting framework to attach comments to the correct fields. The StructBlock’s form template is also responsible for rendering labels for each field, but this (and all other HTML markup) can be customized as you see fit. The template below replicates the default StructBlock form rendering:

{% load wagtailadmin_tags  %}

<div class="{{ classname }}">
    {% if help_text %}
        <span>
            <div class="help">
                {% icon name="help" classname="default" %}
                {{ help_text }}
            </div>
        </span>
    {% endif %}

    {% for child in children.values %}
        <div class="w-field" data-field data-contentpath="{{ child.block.name }}">
            {% if child.block.label %}
                <label class="w-field__label" {% if child.id_for_label %}for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}{% if child.block.required %}<span class="w-required-mark">*</span>{% endif %}</label>
            {% endif %}
            {{ child.render_form }}
        </div>
    {% endfor %}
</div>

Additional JavaScript on StructBlock forms

Often it may be desirable to attach custom JavaScript behaviour to a StructBlock form. For example, given a block such as:

class AddressBlock(StructBlock):
    street = CharBlock()
    town = CharBlock()
    state = CharBlock(required=False)
    country = ChoiceBlock(choices=[
        ('us', 'United States'),
        ('ca', 'Canada'),
        ('mx', 'Mexico'),
    ])

we may wish to disable the ‘state’ field when a country other than United States is selected. Since new blocks can be added dynamically, we need to integrate with StreamField’s own front-end logic to ensure that our custom JavaScript code is executed when a new block is initialized.

StreamField uses the telepath library to map Python block classes such as StructBlock to a corresponding JavaScript implementation. These JavaScript implementations can be accessed through the window.wagtailStreamField.blocks namespace, as the following classes:

  • FieldBlockDefinition

  • ListBlockDefinition

  • StaticBlockDefinition

  • StreamBlockDefinition

  • StructBlockDefinition

First, we define a telepath adapter for AddressBlock, so that it uses our own JavaScript class in place of the default StructBlockDefinition. This can be done in the same module as the AddressBlock definition:

from wagtail.blocks.struct_block import StructBlockAdapter
from wagtail.telepath import register
from django import forms
from django.utils.functional import cached_property

class AddressBlockAdapter(StructBlockAdapter):
    js_constructor = 'myapp.blocks.AddressBlock'

    @cached_property
    def media(self):
        structblock_media = super().media
        return forms.Media(
            js=structblock_media._js + ['js/address-block.js'],
            css=structblock_media._css
        )

register(AddressBlockAdapter(), AddressBlock)

Here 'myapp.blocks.AddressBlock' is the identifier for our JavaScript class that will be registered with the telepath client-side code, and 'js/address-block.js' is the file that defines it (as a path within any static file location recognized by Django). This implementation subclasses StructBlockDefinition and adds our custom code to the render method:

class AddressBlockDefinition extends window.wagtailStreamField.blocks
    .StructBlockDefinition {
    render(placeholder, prefix, initialState, initialError) {
        const block = super.render(
            placeholder,
            prefix,
            initialState,
            initialError,
        );

        const stateField = document.getElementById(prefix + '-state');
        const countryField = document.getElementById(prefix + '-country');
        const updateStateInput = () => {
            if (countryField.value == 'us') {
                stateField.removeAttribute('disabled');
            } else {
                stateField.setAttribute('disabled', true);
            }
        };
        updateStateInput();
        countryField.addEventListener('change', updateStateInput);

        return block;
    }
}
window.telepath.register('myapp.blocks.AddressBlock', AddressBlockDefinition);

Additional methods and properties on StructBlock values

When rendering StreamField content on a template, StructBlock values are represented as dict-like objects where the keys correspond to the names of the child blocks. Specifically, these values are instances of the class wagtail.blocks.StructValue.

Sometimes, it’s desirable to make additional methods or properties available on this object. For example, given a StructBlock that represents either an internal or external link:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

you may want to make a url property available, that returns either the page URL or external URL depending on which one was filled in. A common mistake is to define this property on the block class itself:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    @property
    def url(self):  # INCORRECT - will not work
        return self.external_url or self.page.url

This does not work because the value as seen in the template is not an instance of LinkBlock. StructBlock instances only serve as specifications for the block’s behavior, and do not hold block data in their internal state - in this respect, they are similar to Django’s form widget objects (which provide methods for rendering a given value as a form field, but do not hold on to the value itself).

Instead, you should define a subclass of StructValue that implements your custom property or method. Within this method, the block’s data can be accessed as self['page'] or self.get('page'), since StructValue is a dict-like object.

from wagtail.blocks import StructValue


class LinkStructValue(StructValue):
    def url(self):
        external_url = self.get('external_url')
        page = self.get('page')
        return external_url or page.url

Once this is defined, set the block’s value_class option to instruct it to use this class rather than a plain StructValue:

class LinkBlock(StructBlock):
    text = CharBlock(label="link text", required=True)
    page = PageChooserBlock(label="page", required=False)
    external_url = URLBlock(label="external URL", required=False)

    class Meta:
        value_class = LinkStructValue

Your extended value class methods will now be available in your template:

{% for block in page.body %}
    {% if block.block_type == 'link' %}
        <a href="{{ link.value.url }}">{{ link.value.text }}</a>
    {% endif %}
{% endfor %}

Custom block types

If you need to implement a custom UI, or handle a datatype that is not provided by Wagtail’s built-in block types (and cannot be built up as a structure of existing fields), it is possible to define your own custom block types. For further guidance, refer to the source code of Wagtail’s built-in block classes.

For block types that simply wrap an existing Django form field, Wagtail provides an abstract class wagtail.blocks.FieldBlock as a helper. Subclasses should set a field property that returns the form field object:

class IPAddressBlock(FieldBlock):
    def __init__(self, required=True, help_text=None, **kwargs):
        self.field = forms.GenericIPAddressField(required=required, help_text=help_text)
        super().__init__(**kwargs)

Since the StreamField editing interface needs to create blocks dynamically, certain complex widget types will need additional JavaScript code to define how to render and populate them on the client-side. If a field uses a widget type that does not inherit from one of the classes inheriting from django.forms.widgets.Input, django.forms.Textarea, django.forms.Select or django.forms.RadioSelect, or has customized client-side behavior to the extent where it is not possible to read or write its data simply by accessing the form element’s value property, you will need to provide a JavaScript handler object, implementing the methods detailed on Form widget client-side API.

Handling block definitions within migrations

As with any model field in Django, any changes to a model definition that affect a StreamField will result in a migration file that contains a ‘frozen’ copy of that field definition. Since a StreamField definition is more complex than a typical model field, there is an increased likelihood of definitions from your project being imported into the migration – which would cause problems later on if those definitions are moved or deleted.

To mitigate this, StructBlock, StreamBlock, and ChoiceBlock implement additional logic to ensure that any subclasses of these blocks are deconstructed to plain instances of StructBlock, StreamBlock and ChoiceBlock – in this way, the migrations avoid having any references to your custom class definitions. This is possible because these block types provide a standard pattern for inheritance, and know how to reconstruct the block definition for any subclass that follows that pattern.

If you subclass any other block class, such as FieldBlock, you will need to either keep that class definition in place for the lifetime of your project, or implement a custom deconstruct method that expresses your block entirely in terms of classes that are guaranteed to remain in place. Similarly, if you customize a StructBlock, StreamBlock, or ChoiceBlock subclass to the point where it can no longer be expressed as an instance of the basic block type – for example, if you add extra arguments to the constructor – you will need to provide your own deconstruct method.