Wagtail-roadrunner is a new type of page editor for wagtail. With the wagtail-roadrunner interface, blocks can be laid out in a grid instead of in a single horizontal sequence. The grid in wagtail-roadrunner is the bootstrap grid, so it has 12 segments per row. Columns can be specified with widths from 1-12 segments.
Columns are laid out horizontally in rows, and blocks can be added to each column which will be placed in the column below each other.
Wagtail-roadrunner is not a full custom user interface for wagtail, it is just a new type of streamfield type, which has it's own rendering using telepath.
So the grid interface can be mixed on pages with the standard interface.
To make the editing interface as uncluttered as possible, wagtail-roadrunner does not display the full block form when opening a page. Instead a preview of the content is displayed and the editing interface is opened when cliking on a block.
This make it easier to find the content you want to change, while offering a large editing window with the block forms. Columns would simply become too narrow to fit the full form interface.
Wagtail-roadrunner offers a couple of ways for you to define previews for your blocks, which are documented below.
To install the latest version from pypi
pip install wagtail-roadrunner
Now add the roadrunner app to your INSTALLED_APPS
setting in your django settings:
INSTALLED_APPS = [
# all kinds of apps
# And now ad the roadrunner app like this:
"rr",
# Add table block if you plan on using our table block.
"wagtail.contrib.table_block",
]
wagtail-roadrunner comes with a new streamfield type, RoadRunnerField
which
can be used in a wagtail page like this:
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core.models import Page
from wagtail.wagtailsearch import index
from rr.fields import RoadRunnerField
class GridPage(Page):
content = RoadRunnerField(null=True, blank=True)
content_panels = Page.content_panels + [StreamFieldPanel("content")]
search_fields = Page.search_fields + [
index.SearchField("content", partial_match=True),
]
This will create a page with only one field, with the default blocks that ship with wagtail-roadrunner. There are 2 ways to change the blocks offered to the user.
The first one is to just specify the blocks like in a StreamField:
from wagtail.admin.edit_handlers import StreamFieldPanel
from wagtail.core.models import Page
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock
# your own code
from .myblocks import GalleryBlock
class GalleryPage(Page):
gallery = RoadRunnerField(
[
("paragraph", blocks.RichTextBlock()),
("heading", blocks.CharBlock()),
("image", ImageChooserBlock()),
("gallery", GalleryBlock()),
]
)
content_panels = Page.content_panels + [StreamFieldPanel("gallery")]
In this example, only 4 type of blocks will be available, but still laid out in a grid.
It is also possible to change the default block set that is used by RoadRunnerField. This way of registering blocks with wagtail-roadrunner makes reusing your blocks easy. Making reuase easy, makes it worthwhile to spend some extra effort on your blocks to make them as user freindly as possible.
To register blocks with wagtail-roadrunner you should define your own
ROADRUNNER_REGISTRY_FUNCTION
.
This function is passed the default blocks that come with wagtail-roadrunner, so
if you want you can drop some functionality you don't need. Here is an example:
# a file named myproject/registry.py
from .myblocks import GalleryBlock, TetrisBlock
def register_custom_blocks(initial_blocks):
initial_blocks # or maybe filter the initial block to remove some?
+ [
("gallery", GalleryBlock()),
("tetris", TetrisBlock()),
]
You then have to register this function with wagtail-roadrunner as django setting like this:
ROADRUNNER_REGISTRY_FUNCTION = "myproject.registry.register_custom_blocks"
We don't want to display the full block form in the page editor, just a small preview that is enough for a user to be able to recognise the content. There are 3 ways to define a preview for your block, from simple to advanced these are:
- Use the
meta.preview
property, that lists the fields you want to show. - Define
meta.preview_template
. - Add a
renderPreview
method to your telepath Block definition in javascript.
Using the meta.preview
property is done like this:
class ImageBlock(blocks.StructBlock):
image = ImageChooserBlock()
alt = blocks.CharBlock(
max_length=255,
label="Alt.",
help_text="Optioneel, afbeelding alt tekst",
required=False,
)
lazy = blocks.BooleanBlock(label="Lazy", default=False, required=False)
page_url = blocks.PageChooserBlock(label="Pe url", required=False)
external_url = blocks.CharBlock(label="External link", required=False)
open_in_new_tab = blocks.BooleanBlock(
label="Open in een nieuwe tab", required=False
)
class Meta:
preview = ["image"]
This block has a lot of fields that are more configuration than content. For a
user it is enough to just show the image as the preview. This is easily achieved
by specifying the meta.preview
property as:
class Meta:
preview = ["image"]
That reduces the clutter in the page editor interface by a lot! Instead of
showing all the form fields on the page, we just show the most important field
that makes the content recognisable for the user. The meta.preview
property
is a list, suppose we also want to make the alt
field visible to the user,
we could change it to:
class Meta:
preview = ["image", "alt"]
Using the meta.preview_template
property is done like this:
class AccordionBlock(blocks.StructBlock):
header = blocks.CharBlock(
max_length=255,
label="Title",
help_text="Header of the accordion",
required=False,
)
panel_content = blocks.RichTextBlock(
label="Inhoud", help_text="Body of the accordion", required=False
)
class Meta:
preview_template = "preview/bootstrap/accordion.html"
meta.preview_template
defines the preview template for this specific block as
preview/bootstrap/accordion.html. This template will be used to render the preview.
preview_template
acts much the same as form_template
(see https://docs.wagtail.org/en/latest/advanced_topics/customisation/streamfield_blocks.html#how-to-build-custom-streamfield-blocks).
The template context is the same, with the most important variable:
children
An OrderedDict of BoundBlocks for all of the child blocks making up this StructBlock.
While the preview does not actually render any forms, the same render_form
method should be used to render the preview of a formfield in the preview_template
.
<div class="accordion_preview">
{{ children.header.render_form }}
{{ children.panel_content.render_form }}
</div>
Here you can add your own (bootstrap) classes and/or html tags to mirror the output of your block. The fields are always accessed like this:
children.field_name.render_form
You can create your own preview UI in javascript by registering your own block definition with telepath.
There are 2 ways to include wagtail-roadrunner in your project:
- you can use the exported symbols on
window
, the entire library is available aswindow.roadrunner
, just likewindow.wagtailStreamField
. - Alternatively you can install wagtail-roadrunner as a dependency in your javascript project like this:
npm add highbiza/wagtail-roadrunner
This will add the wagtail-roadrunner github repo's javascript to your project. You can now import the classes from wagtail-roadrunner in your js files.
Wagtail-roadrunner does use jsx, but without React because of the way wagtail's
widgets work. They need to be rendered immediately into a placeholder div,
which would become very tedious in React. wagtail-roadrunner uses jsx-render,
which provides a jsx renderer implementation. Use the renderInPlaceHolder
and PlaceHolder
utils from roadrunner in your project.
Here is an example of the PageTitle
UI, shipped with roadrunner but
modified to fit in your project. It has been changed to Use a <heading>
instead of a <h1>
. We could have subclasses the existing PageTitle js,
but this is more similar to how things would look for a completely custom
component started from scratch.
import $ from "jquery"
import dom, { Fragment } from 'jsx-render'
// Use object destructuring to get the symbols off of window:
const {
roadrunner: {
jsx: { renderInPlaceHolder, PlaceHolder },
preview: {
render: { Preview }
}
}
} = window
/**
* or you could also import these symbols from the library if you added
* wagtail-roadrunner to your project's dependencies
*/
import { renderInPlaceHolder, PlaceHolder } from "roadrunner/jsx"
import { Preview } from "roadrunner/preview/render"
/**
* now you can use the imported symbols and write your own code:
*/
class MyPageTitle extends Preview {
getValue() {
return $("h1").first()
.text()
}
render(previewPlaceholder, prefix, initialState, initialError) {
return renderInPlaceHolder(previewPlaceholder,
<Fragment>
<div id={prefix} class="preview-label">
<heading>{this.getValue()}</heading>
</div>
<PlaceHolder/>
</Fragment>
)
}
}
export class MyPageTitleDefinition extends window.wagtailStreamField.blocks.StructBlockDefinition {
renderPreview(previewPlaceholder, prefix, initialState, initialError) {
return new PageTitle(this, previewPlaceholder, prefix, initialState, initialError)
}
}
// register the MyPageTitleDefinition class with telepath as "myproject.PageTitleDefinition"
window.telepath.register("myproject.PageTitleDefinition", MyPageTitleDefinition)
To use this code, we should register an Adapter
with telepath. Let's not
start from scratch, but override the adapter shipped with roadrunner. It is
always best to find out which Adapter is used in wagtail for similar components
and subclass the Adapter for that type:
from rr.adapters import PageTitleAdapter
class MyCustomPageTitleAdapter(PageTitleAdapter):
js_constructor = "myproject.PageTitleDefinition"
To register the Adapter, which will make wagtail use our javascript for objects of the registered type, we need to descide if we want to use it in a narrow or wide scope.
Wide scope means, everywhere in wagtail, when a object of this type is encountered, use this javascript. Narrow scope means only when encountered in a roadrunner context, use this javascript.
Narrow registration:
from rr.telepath import register
from rr.blocks.html import PageTitle
register(MyCustomPageTitleAdapter(), PageTitle)
If we want to use this code everywhere, just do:
from wagtail.core.telepath import register
from rr.blocks.html import PageTitle
register(MyCustomPageTitleAdapter(), PageTitle)
By default, wagtail will index all blocks that have implemented the get_searchable_content method. It could be that not all fields on a block are meant to be searchable content. In that case you should make your own get_searchable_content implementation.
In our default blocks, we have implemented the get_searchable_content method already, ignoring fields that should not be searched.
To make implementing your own get_searchable_content
easier, we have added a utility function for you in rr.search
:
def get_searchable_content_for_fields(value, child_blocks, index_fields):
content = []
for block_key, block in child_blocks.items():
if block_key in index_fields:
content.extend(block.get_searchable_content(value[block_key]))
return content
We use this in the SliderChildBlock
and HeaderBlock
block.