Skip to content

highbiza/wagtail-roadrunner

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wagtail-roadrunner

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.

Installation

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",
]

Usage

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"

Making previews

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:

  1. Use the meta.preview property, that lists the fields you want to show.
  2. Define meta.preview_template.
  3. Add a renderPreview method to your telepath Block definition in javascript.

The preview property

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"]

The preview_template property

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

The renderPreview method

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:

  1. you can use the exported symbols on window, the entire library is available as window.roadrunner, just like window.wagtailStreamField.
  2. 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)

Block searchable content

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.