Wagtail ‘TumblrField’ spec Motivation A common requirement for Wagtail sites is the ability to create ‘freeform’ content consisting of different content types arranged in any order: headings, paragraphs, images, video, and potentially others, including custom types specific to that site. The current goto solution for this is the rich text area, but this is undesirable for several reasons: ● It encourages people to disregard separation of content and design ● Browser support for ‘contenteditable’ elements is often quirky, particularly when dealing with floated elements or blocks of noneditable text, and fixing the resulting bugs is either impossible or requires digging into hallo.js code ● It is difficult to extend to support new content types (resulting in developers adopting lessthanideal solutions like permitting arbitrary blobs of HTML within the field) This spec proposes a new mechanism provisionally named TumblrField, inspired by Wagtail’s existing ‘InlinePanel’ model along with Sir Trevor and Tumblr, which allows editing freeform content as a sequence of interchangeable content blocks.
Scope ●
A component for the page editor that allows building a mixed sequence of content types, that serialises to a single JSON field in the database ● A straightforward way to output the entire chunk of content on a template, using HTML representations defined by each content type ● A clean API for building new content types for use within this component
Out of scope ● ●
Nested TumblrFields (for multicolumn arrangements of content) Native database representation of the content (e.g. giving each content item its own database record) ● Interaction between different items in the sequence (e.g. cursor movement between adjacent paragraphs; pressing enter at the end of a paragraph block to spawn another paragraph block)
Functionality A page type incorporating a TumblrField might be defined like this (NB names and syntax are subject to change): BlogPage.content_panels = [ FieldPanel('title'), TumblrFieldPanel('content', block_types=[ ('heading', HeadingBlock()),
('paragraph', ParagraphBlock()), ('sideimage', ImageBlock(formats=['left', 'right'])), ('leadimage', ImageBlock(formats=['fullwidth'])), ]) ]
This will provide an interface functionally equivalent to http://madebymany.github.io/sirtrevorjs/example.html, with four block types available: ● a 'heading' block, consisting of a single (nonrich) text field; ● a 'paragraph' block, consisting of a rich text area with limited formatting capabilities such as bold / italic text and links, but not image embedding or heading types; ● a 'side image' block, consisting of an image chooser, a text field to specify alt text, and a select box (or pair of radio buttons) to specify whether it will be left or right justified; ● a 'lead image' block, consisting of an image chooser and a text field to specify alt text (there is no format select box, since there is only one option available). On submitting the form, these fields will be validated according to each block type's validation rules, if any. If validation succeeds, the data will be written to the 'content' field of the model as a JSON string: [ {'type': 'heading', 'value': 'Fish found on moon'}, {'type': 'leadimage', 'value': {'id': 123, 'alt': 'a fish', 'format': 'fullwidth'}}, {'type': 'paragraph', 'value': 'A fish of species
verasper variegetus was found on the moon yesterday.'}, {'type': 'paragraph', 'value': 'NASA were unavailable for comment.'} ]
The 'type' field indicates which of the block types (as specified within the TumblrFieldPanel definition) is responsible for this content block; 'value' is an arbitrary JSON value assigned and understood by that specific block type. It will be possible to render the content on a template simply by using the tag {{ self.content }}. Each block type defines a way to render its data in HTML form (e.g. as a suitablystyled
element for ImageBlock), and the HTML rendering for the TumblrField as a whole simply loops over these renderings. It will also be possible to loop over the blocks manually if more control over the output is required: {% for block in self.content %}
{{ block }}
{% endfor %}
As well as providing their own HTML representation, blocks will also expose the raw data for templates to display using their own rendering. This might look something like: {% tumblrfield self.content %} {% blocktype 'heading' %}
{{ heading.text }}
{% endblocktype %} {% blocktype 'sideimage' as image %}
{% endblocktype %} {# all block types not mentioned here fall back on their default HTML renderings #} {% endtumblrfield %}
Here {% tumblrfield %} and {% blocktype %} are custom tags that act as a kind of switch/case statement, invoking the appropriate section for each content block in the sequence depending on its type. Wagtail site implementers will be able to create custom block types. We will provide a number of abstract 'block type' base classes on top of which developers will be able to implement blockspecific behaviours, such as: the rendering of the field(s) within the edit form; form validation; the block's default HTML rendering; the way it is serialised and deserialised to and from JSON; and the styling of the button within the TumblrField's toolbar.
Development notes The API required for block types somewhat resembles Wagtail's existing EditHandler API, and Django's form field API. It may be that one or the other of these is a close enough fit for us to use it asis, or can be extended to fit our needs; alternatively, we may create a new API inspired by these. (The EditHandler API is one area of Wagtail that probably needs some review it was developed in a rather adhoc way, with constraints dictated by Wagtail’s visual design and Django internals. As part of this development, we will consider whether the new block types API can serve as a starting point for a new EditHandler implementation. EditHandler will not be dropped entirely in this phase of development indeed, TumblrFieldPanel will be implemented as an EditHandler.)