Ability to arrange StructBlock sub-blocks in rows or groups, with control over ordering
See original GitHub issueIs your proposal related to a problem?
With panels/edit handlers, we have FieldRowPanel
and MultiFieldPanel
to help add some useful structure to complicated models, but there is no equivalent for blocks, which means StructBlock
rendering is syntactically and visually flat by default.
It would be great if Wagtail had a baked-in way to add more visual and syntactical structure to the UI presented by a StructBlock
.
Describe the solution you’d like
You could add a StuctBlockLayout
to a block’s Meta
class to define the layout you like. You could either use block name strings to represent each block, like so:
class ExampleBlock(block.StructBlock):
...
class Meta:
layout = StructBlockLayout("block_one", "block_two", "block_three")
OR, provide BlockPlaceholder()
instances when more control is needed:
class ExampleBlock(block.StructBlock):
...
class Meta:
layout = StructBlockLayout(
BlockPlaceholder("block_one", classname="foo"),
BlockPlaceholder("block_two", classname="bar"),
"block_three",
"block_four",
)
Using BlockRow
to add a row of blocks
If you wanted to group blocks into a row/grid layout, you would add a BlockRow
instance to your layout. For MVP, you would simply provide the block placeholders in the order you wanted them to appear, and classes would be automatically applied to display them in a grid of equal-sized columns, that would break down sensibly at different screen sizes.
StructBlockLayout(
BlockRow("block_one", "block_two", "block_three"),
)
Using BlockGroup
to group blocks under a heading
You could group block fields into a fieldset like so:
BlockGroup(
"block_one",
"block_two",
"block_three",
"block_four" ,
heading="Advanced options",
icon="fa-cog",
help_text="An optional description that will be displayed before the sub-blocks",
classname="group__advanced",
)
Alternatively, you could pass a list of sub-block placeholders/rows as a list, using the children
keyword argument (like you can for MultiFieldPanel), e.g:
BlockGroup(
heading="Advanced options",
icon="fa-cog",
help_text="An optional description that will be displayed before the sub-blocks",
classname="group__advanced",
children=[
BlockRow("block_one", "block_two", "block_three"),
BlockPlaceholder("block_four"),
]
)
What about nesting?
A BlockRow
could be nested within a BlockGroup
, but not vice versa.
Attempting to nest a BlockRow
or BlockGroup
within another would raise an exception.
What about blocks not included in the layout?
We always want to show all sub-blocks defined for a StructBlock
(if they aren’t needed, they should be ‘nulled out’ using block_name = None
), so if any blocks are ommited from a layout, they would automatically be output at the end of the block’s form.
What if a named block doesn’t exist?
I think the most sensible thing to do here is silently ignore these cases. With this level of control over layout available, I would imagine it would be more common to subclass blocks for reuse (the current lack of control over block order makes this more hassle than it is worth). This, combined with the fact that layouts would be inherited, would make it painful should you need to ‘null out’ blocks in a sub-block, but would otherwise like the layout to remain the same. If it raised an error, that would mean having to completely redefine a StructBlockLayout
in these cases, which I feel is unhelpful.
One drawback of this approach is: There would be no protection against spelling errors / accidental mismatches at the Python level. Developers would have to load the UI and look at the output to confirm all of the fields were appearing as expected. Any mismatched blocks would appear at the bottom, so should hopefully be easy to spot.
How would this actually work?
Like blocks in a Streamfield definition, the actual BlockPlaceholder
, BlockRow
, BlockGroup
instances used in the definition exist for as long as the app instance is running. So, to guarantee thread-safety, should not be bound to request or value-specific values. However, they should be able to implement a render
method that accepts all of the necessary values, does any necessary processing, and renders the values a template.
Describe alternatives you’ve considered
At first, I thought we might be able to achieve this with new block types, (with signatures similar that of StreamBlock
), that would become part of the block definition. However, I think this is a bad idea because:
- It’s mixing data and presentation, which is icky.
- It complicates inheritance. If you want the same block types in a sub-class, but want to present them in a different way, there’s no easy way to do that.
- It would lead to even bigger data migrations than are generated already, for data that is irrelevant for migrations.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:2
- Comments:13 (7 by maintainers)
Top GitHub Comments
With the new page editor designs / #8983, I believe displaying blocks in rows should be easier now that StreamField blocks’ usage of space is much more efficient (blocks are indented to the left only).
Here is a quick prototype, just applying
display: grid; grid-auto-flow: column;
to the blocks’ parent element:The main issue will be with the placement of the “Add” buttons in-between blocks. We’ve made them much smaller, and will soon™ also completely change the “add” UI when opened – so this shouldn’t be an issue for much longer.
Note from an accessibility standpoint I would recommend to keep usage of this row layout in forms to a minimum, as it can be problematic for users of magnification software, who might not realise they have to go through the page both top to bottom and left to right.
@awhileback be careful what you call “clever” — our dynamic use of generating streamfield dropdown values has created some major problems with Django migrations because it is too “clever” (or something like that) 🤣