Typed table block

The typed_table_block module provides a StreamField block type for building tables consisting of mixed data types. Developers can specify a set of block types (such as RichTextBlock or FloatBlock) to be available as column types; page authors can then build up tables of any size by choosing column types from that list, in much the same way that they would insert blocks into a StreamField. Within each column, authors enter data using the standard editing control for that field (such as the Draftail editor for rich text cells).

Installation

Add "wagtail.contrib.typed_table_block" to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    "wagtail.contrib.typed_table_block",
]

Usage

TypedTableBlock can be imported from the module wagtail.contrib.typed_table_block.blocks and used within a StreamField definition. Just like StructBlock and StreamBlock, it accepts a list of (name, block_type) tuples to use as child blocks:

from wagtail.contrib.typed_table_block.blocks import TypedTableBlock
from wagtail import blocks
from wagtail.images.blocks import ImageChooserBlock

class DemoStreamBlock(blocks.StreamBlock):
    title = blocks.CharBlock()
    paragraph = blocks.RichTextBlock()
    table = TypedTableBlock([
        ('text', blocks.CharBlock()),
        ('numeric', blocks.FloatBlock()),
        ('rich_text', blocks.RichTextBlock()),
        ('image', ImageChooserBlock())
    ])

To keep the UI as simple as possible for authors, it’s generally recommended to use Wagtail’s basic built-in block types as column types, as above. However, all custom block types and parameters are supported. For example, to define a ‘country’ column type consisting of a dropdown of country choices:

table = TypedTableBlock([
    ('text', blocks.CharBlock()),
    ('numeric', blocks.FloatBlock()),
    ('rich_text', blocks.RichTextBlock()),
    ('image', ImageChooserBlock()),
    ('country', ChoiceBlock(choices=[
        ('be', 'Belgium'),
        ('fr', 'France'),
        ('de', 'Germany'),
        ('nl', 'Netherlands'),
        ('pl', 'Poland'),
        ('uk', 'United Kingdom'),
    ])),
])

On your page template, the {% include_block %} tag (called on either the individual block, or the StreamField value as a whole) will render any typed table blocks as an HTML <table> element.

{% load wagtailcore_tags %}

{% include_block page.body %}

Or:

{% load wagtailcore_tags %}

{% for block in page.body %}
    {% if block.block_type == 'table' %}
        {% include_block block %}
    {% else %}
        {# rendering for other block types #}
    {% endif %}
{% endfor %}

Custom validation

As with other blocks, validation logic on TypedTableBlock can be customised by overriding the clean method (see StreamField validation). Raising a ValidationError exception from this method will attach the error message to the table as a whole. To attach errors to individual cells, the exception class wagtail.contrib.typed_table_block.blocks.TypedTableBlockValidationError can be used - in addition to the standard non_block_errors argument, this accepts a cell_errors argument consisting of a nested dict structure where the outer keys are row indexes and the inner keys are column indexes. For example:

from django.core.exceptions import ValidationError
from wagtail.blocks import IntegerBlock
from wagtail.contrib.typed_table_block.blocks import TypedTableBlock, TypedTableBlockValidationError


class LuckyTableBlock(TypedTableBlock):
    number = IntegerBlock()

    def clean(self, value):
        result = super().clean(value)
        errors = {}
        print(result.row_data)
        for row_num, row in enumerate(result.row_data):
            row_errors = {}
            for col_num, cell in enumerate(row['values']):
                if cell == 13:
                    row_errors[col_num] = ValidationError("Table cannot contain the number 13")
            if row_errors:
                errors[row_num] = row_errors

        if errors:
            raise TypedTableBlockValidationError(cell_errors=errors)

        return result