WYSIWYG

Setting Up RichText Streamfields

In Wagtail there are two ways to add RichText to your pages. In a previous article called Setting Up A RichText Content Area we explored how to add a RichText field as a model field to your page.

In this article we're going to add a RichText WYSIWYG editor to your streamfields for much more flexible content. That is, we're going to let the content managers place their richtext anywhere they like on the page.

If you're uncertain what a Streamfield is then you should read Wagtails page on Steramfields. It does a much better job of describing it's power than I typically do.

Adding a Streamfield

The first type of StreamField we're going to go over in this article is the basic StreamField. It can have multiple fields, much like a regular Django model, although we build it out differently than a regular Django model field.

Let's dive into some code and then I'll explain it all (I suggest you read through the code, line by line, just to get an idea of what's happening before the explanation; it helps with learning.)

"""Demo page."""
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.models import Page


class DemoPage(Page):
    """A demo page class."""

    template = "demo/demo_page.html"

    content_panels = [
        FieldPanel("title", classname="full title"),
    ]

    class Meta:
        """Meta information."""

        verbose_name = "Demo Page"
        verbose_name_plural = "Demo Pages"

Above is our demo page. Nothing major, it's a basic Wagtail page. But let's walk through this. Here's what this code does:

  1. Import FieldPanel and a Page class
  2. Create a new class called DemoPage (you'll need to run migrations with this)
  3. Set a template path and file
  4. Told Wagtail to only show the Page.title field in the content_panels
  5. Added a verbose name and a plural name in the Meta detail

Nothing crazy going on. So let's go ahead and add a StreamField section with a StreamFieldPanel.

"""Demo page."""
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField
from wagtail.core.models import Page

from streams import blocks
# If pathing isn't a strong skill yet, you can use this instead:
# from . import blocks


class DemoPage(Page):
    """A demo page class."""

    template = "demo/demo_page.html"

    content = StreamField(
        [
            ("custom_name", blocks.CustomRichTextBlock()),
            # ... aditional streamfields in here. Copy the line
            # .. above these comments and adjust the names as needed
        ],
        null=True,
        blank=True,
    )

    content_panels = [
        FieldPanel("title", classname="full title"),
        StreamFieldPanel("content"),
    ]

    class Meta:
        """Meta information."""

        verbose_name = "Demo Page"
        verbose_name_plural = "Demo Pages"

This looks like a lot more. But it's not that much more, and I promise this will make more sense by the end of this tutorial.

The differences from the initial DemoPage class to this new version are:

  1. We've imported StreamField
  2. We're assuming there's an app called streams and it has a file called blocks.py (see the comments below that line if that's beyond your skills right now)
  3. We've added a field called content (It's not a special name. I've seen others use the word body instead of content ¯\_(ツ)_/¯)
    1. Inside the content field is a StreamField object, with a list of tuples. The first item in the tuple is the SteamField name. Feel free to call this anything you like. I like to generally keep it descriptive.
    2. The second item in the tuple is a class called CustomRichTextBlock that comes from the blocks.py file we imported in the second bullet point
    3. Repeat as you have more StreamFields to add to your page
  4. Lastly, we added StreamFieldPanel("content"), to the content_panels so Wagtail will give the StreamFields on our pages when we create a new page, or edit an existing page.

If you run this code right now, it will break. blocks.py probably doesn't exist yet, and if it does, then CustomRichTextBlock probably doesn't. So let's go make that now.

You'll need to create a new Django app called streams (the name is unimportant as long as you import it correctly) and create a new file called blocks.py in it. Or if you want to keep this tutorial simple, create a new file beside your models.py file called blocks.py. Either way, you'll need a blocks.py file and you'll need to import it.

Inside of the blocks.py file add the following code:

"""blocks.py: Stream Blocks."""
from wagtail.core import blocks


class CustomRichTextBlock(blocks.StructBlock):
    """Rich text content."""

    richtext_content = blocks.RichTextBlock(required=True)

    class Meta:
        """Provide additional meta information."""

        template = "streams/custom_richtext_block.html"
        icon = "edit"
        label = "Custom Richtext"

Here we're not doing anything crazy either. In fact when you break this down, this is doing A LOT of work for you with very little effort.

  1. Import wagtail.core's blocks.py file
  2. Create a new class called CustomRichTextBlock and inherit blocks.StructBlock (this is how Wagtail knows this is one of the StreamField types
  3. Added a new model field called richtext_content and said it needs to be a RichtTextBlock, and that it's required
  4. Added a template and icon to the StreamField

If you save your files, run and apply migrations, and restart Django, you should be able to create a new page called Demo Page. And the content will look like this:

streamfield-preview.png

What DemoPage looks like

stremfield-preview-expanded.png

What the WYSIWYG editor looks like when this StreamField is expanded

If you see what I'm seeing, you've successfully added a RichText StreamField to your DemoPage. If nothing was working for you, open your terminal and check the Django error outputs. The traceback should, hopefully, lead you right back to the issue.

At this point if you preview or view the live page you'll get an error saying there's a missing template. No big deal, let's add the demo_page.html template and then we'll add the custom_richtext_block.html template.

{# demo_page.html %}
{% extends 'base.html' %}
{% load wagtailcore_tags %}

{% block content %}
  {% for block in page.content %}
    {# Loop through every streamfield and include the rendered template files to your page. #}
    {% include_block block %}
  {% endfor %}
{% endblock content %}

We got the name demo_page.html from the DemoPage class we created at the start of this tutorial. The template property tells us where to find this file. If you're new to Django and Wagtail, this template file should be under templates/demo/demo_page.html

Now let's add the RichText StreamField template.

Fun fact: all streamfields can use a template file, get rendered, and be injected into your for loop in your demo_page.html template.

{# templates/streams/custom_richtext_block.html #}
<div style='padding: 50px;'>
  {{ self.richtext_content }}
</div>

That's all there is to setting up StreamFields, really. And this one takes care of rendering out the WYSIWYG data too.

Bonus Feature:
Wagtail will also strip unaccepted elements from the WYSIWYG editor too. So if you think someone might be trying to add a <script> in there and perform an XSS attack on your website, it won't work. Wagtail's editor (called Draftail) will strip it out. Security is sexy.

Your page will now have rendered RichText on it.

richtext-streamfield-live-preview.png

Sample RichText I added with markup via the StreamField's RichTextBlock

Adding Additional Fields

More often than not, you'll want to add more than just RichText. Maybe you want a special image, or a special title, or to give the content entry people a way to select the background color. It could be any number of reasons, but let's go with: content entry people want to change the background color.

Simple enough. All we do is add a Wagtail ChoiceBlock and put it in to the template. Add the ChoiceBlock to your StreamField, like so:

class CustomRichTextBlock(blocks.StructBlock):
    """Rich text content."""

    richtext_content = blocks.RichTextBlock(required=True)
    bg_color = blocks.ChoiceBlock(required=True, choices=[
        ('white', 'White'),
        ('blue', 'Blue'),
        ('black', 'Black'),
    ], default='white')
    ....

And when you save and reload your DemoPage, you'll see an option like this:

richtext-background-image-options.png

At the bottom is a drop down (select field) that lets you pick the color

Pick a color. By default we said the background color should be white. But let's make it really gross so it stands out and we know if it's working or not... let's make it browser-default blue. Open up your StreamField template and add the color.

{# templates/streams/custom_richtext_block.html #}
<div style='padding: 50px; background-color: {{ self.bg_color }}'>
  {{ self.richtext_content }}
</div>

All we did was add {{ self.bg_color }} inside the inline CSS. You can do this will full CSS classes since it's cleaner that way. But you can also perform logic on this such as {% if self.bg_color == 'blue' %}color: white;{% endif %} and that'd change the text color to white, with a blue background. We didn't add that logic in, so let's take a look at this terrible design.

1998-called-they-want-their-design-back.png

1998 called and they want their design back

So as we can see, we've now built a StreamField with a RichTextBlock and a ChoiceBlock to give the content entry people a choice of background color.

But wait, there's more!

Using the RichTextBlock by itself

What if we just wanted to give people a RichText editor in their StreamFields without any additional fields? In this case, let's say the background color was never going to change and it's simply a WYSIWYG editor that outputs RichText somewhere in the sea of StreamFields.

Let's create a second RichText StreamField. The following code belongs inside of blocks.py.

# blocks.py 

# Other StreamFields  here

class CustomAloneRichTextBlock(blocks.RichTextBlock):

    class Meta:
        template = "streams/custom_alone_richtext_block.html"
        icon = "edit"
        label = "Custom [Alone] Richtext"

The biggest change here is that we didn't inherit blocks.StructBlock, but instead we inherited blocks.RichTextBlock directly. The Wagtail Core team did a fantastic job making StreamFields so darn flexible that we can directly inherit a StreamField without needing to make everything a StructBlock.

But this won't work.. not yet. Open your demo/models.py file (where the DemoPage class lives) and add this StreamField to your content field.

class DemoPage(Page):
    # .... other fields 
    content = StreamField(
        [
            ("custom_name", blocks.CustomRichTextBlock()),
            ("alone_richtext", blocks.CustomAloneRichTextBlock()), # We added this one
            # .... more streams here
        ],
        null=True,
        blank=True,
    )

Save and reload your Demo Page inside the Wagtail Admin. And you'll see two RichText StreamField options now. And when they're both expanded, they look something like this:

two-scoops-of-richtext.png

The first one (top) has the bg_color field. The bottom is JUST a RichTextBlock. It's much cleaner.

The last thing we need is a new template. If you recall, we set the StreamField template of our independent RichTextBlock to point to streams/custom_alone_richtext_block.html. So let's create that file now.

{# streams/custom_alone_richtext_block.html #}
<div style='padding: 50px;'>
  {{ self }}
</div>

When it's all said and done, you'll have two different StreamFields using the RichTextBlock in different ways.

Before I show you what the final page looks like, remember we created a StreamField with the ugly blue background. I've kept it in this image on purpose so we can see the distinct difference between the two different StreamFields.

two-different-richtext-streams.png

StructBlock (multi-field stream) on top; RichTextBlock on the bottom.

The Final Code

Some people want to read the article, some people just want the code. I don't judge ¯\_(ツ)_/¯

Here's a Gist with all the goodies in it.

Was this helpful to you?

Sharing is caring. Help the community by sharing this article.