17. Edit Rich Text

A Rich Textarea allows editing or pasting formatted text, similar to traditional “What you see is what you get” (WYSIWYG) editors. The current implementation offers common text formatting options such as paragraphs, headings, emphasized and bold text, ordered and bulleted lists, and hyperlinks. More text formatting options will be implemented in the future.

The django-formset library provides a widget, which can be used as a drop-in replacement for the HTML element <textarea>, implemented as web component. In a Django form’s CharField, we just have to replace the built-in widget against formset.richtext.widgets.RichTextarea.

from django.forms import fields, forms
from formset.richtext.widgets RichTextarea

class SomeForm(forms.Form):
    text = fields.CharField(widget=RichTextarea)

This widget can be configured in various ways in order to specifically enable the currently implemented formatting options. With the default settings and using the Bootstrap renderer, this textarea will show up like:

RichTextarea widget

17.1. Configuration

When offering a rich textarea, the default formatting options may not be appropriate. Therefore, the widget class RichTextarea can be configured using various control elements.

from formset.richtext import controls
from formset.richtext.widgets RichTextarea

richtext_widget = RichTextarea(control_elements=[
    controls.Bold(),
    controls.Italic(),
])

This configuration would only allow to format text using bold and italic. Currently django-formset implements these formatting options:

Heading

The class formset.richtext.controls.Heading can itself be configured using a list of levels from 1 through 6. Heading([1, 2, 3]) allows for instance, to format a heading by using the HTML tags <h1>, <h2> and <h3>. If provided without parameters, all 6 possible heading levels are available. If only one level is provided, for instance as Heading(1), then the heading button does not provide a pull down menu, but instead is rendered as a single H1 button. This allows placing heading buttons for different levels on the toolbar side by side.

Bold

The class formset.richtext.controls.Bold can be used to format a selected part of the text in bold variant of the font. It can’t be further configured.

Italic

The class formset.richtext.controls.Italic can be used to format a selected part of the text in an emphasized (italic) variant of the font. It can’t be further configured.

Underline

The class formset.richtext.controls.Underline can be used to format a selected part of the text as underlined. This option rarely makes sense. It can’t be further configured.

BulletList

The class formset.richtext.controls.BulletList can be used to format some text as a bulleted list. It can’t be further configured.

OrderedList

The class formset.richtext.controls.OrderedList can be used to format some text as ordered (ie. numbered) list. It can’t be further configured.

HorizontalRule

The class formset.richtext.controls.HorizontalRule can be used to add a horizontal rule between paragraphs of text. It can’t be further configured.

ClearFormat

The class formset.richtext.controls.ClearFormat can be used to remove the current format settings of selected text. It can’t be further configured.

Undo and Redo

The classes formset.richtext.controls.Undo and formset.richtext.controls.Redo can be used to undo and redo changes on the current text. They can’t be further configured.

Link

The class formset.richtext.controls.Link can be used to add a hyperlink to a selected part of some text. When choosing this option, a modal dialog pops up and the user can enter a URL.

Separator

The class formset.richtext.controls.Separator has no functional purpose. It can be used to separate the other buttons visually using a vertical bar.

17.2. Implementation

This richtext area is based on the Tiptap framework. This framework offers many more formatting options than currently implemented by the django-formset library. In the near future I will add many more of those control elements. Please help me to implement them by contributing to these projects.

17.3. Richtext as a Model Field

In the example from above, we used a Django form CharField and replaced the default widget provided by Django (TextInput). A more common use case is to store the entered rich text in a database field. Here django-formset offers two solutions:

17.3.1. Storing rich text as HTML

Storing rich text as HTML inside the database using the field django.db.models.fields.TextField is the simplest solution. It however requires to override the default widget (Textarea) against the RichTextarea provided by django-formset, when instantiating the form associated with this model.

If the content of such a field shall be rendered inside a Django template, do not forget to mark it as “safe”, either by using the function django.utils.safestring.mark_safe or by using the template filter {{ …|safe }}.

17.3.2. Storing rich text as JSON

Since HTML content has an implicit tree structure, an alternative approach to HTML is to keep this hierarchy unaltered when storing. The best suited format for this is JSON. This approach has the advantage, that HTML is rendered during runtime, allowing to adopt the result as needed.

django-formset provides a special model field class formset.richtext.fields.RichTextField. It shall be used as a replacement to Django’s model field class TextField. This model field provides the widget RichTextarea using the default settings. Often that might not be the desired configuration, and it may be necessary to re-declare that widget, while creating the form from the model.

Since the content is stored in JSON, it has to be converted to HTML before being rendered. For this purpose django-formset offers a templatetag, which can be used such as:

{% load richtext %}

{% render_richtext obj.content %}

Here obj is a Django model instance with a field of type RichTextField.

17.3.3. Overriding the Renderer

By postponing the conversion from JSON to a readable format, we can keep our document structure until it is rendered. django-formset provides default templates for this conversion, but you may want to use your own ones:

{% load richtext %}

{% render_richtext obj.content "path/to/alternative/doc.html" %}

The template doc.html is the starting point for each document. Looking at the structure of a rich text document stored in JSON, we see the hierachical structure:

{
    "text": {
        "type": "doc",
        "content": [{
            "type": "paragraph",
            "content": [{
                "type": "text",
                "text": "This is "
            }, {
                "type": "text",
                "marks": [{
                    "type": "bold"
                }],
                "text": "bold"
            }, {
                "type": "text",
                "text": " "
            }, {
                "type": "text",
                "marks": [{
                    "type": "italic"
                }],
                "text": "and italic"
            }, {
                "type": "text",
                "text": " text."
            }]
        }]
    }
}

The type determines the template to use, whereas content is a list of nodes, rendered using their own sub-template determined by their own type.

When rendered by the default richtext/doc.html template, its output looks like:

<p>This is <strong>bold</strong> <em>and italic</em> text.</p>