RFC 94: Union Block
Adds an RFC for adding a UnionBlock to Wagtail.
Related issues:
- https://github.com/wagtail/wagtail/issues/5376
- https://github.com/wagtail/wagtail/issues/381
Note: supersedes #66
This is a really great RFC! I agree that it would be a very valuable addition to Wagtail, and you've very clearly presented why, and thoroughly explained how you would do it. Well done!
One question to confirm my understanding/assumptions: For the link block example, in order to also set the text of the link, you would presumably suggest this sort of structure?
class LinkBlock(StructBlock):
text = CharBlock()
link = UnionBlock([("page", PageChooserBlock()), ("url", URLBlock())])
(or link = MyLinkUnionBlock(), which is defined in its own class above)
I might suggest extending the examples presented to cover that complete link block replacement scenario.
The only bit that I'm a little wary of is introducing this new UI paradigm of using a ChoiceField to choose what you want the sub-block to be. As you point out in referring to wagtail-link-block, this is something that has been done in a number of packages (or custom implementations), but we may want to discuss this UX with @benenright or other design stakeholders before adopting it in Wagtail core.
Thanks for the feedback @Scotchester !
Yes, the example you point out is incomplete (and your assumption is correct) - this is an oversight and I will update it.
I will be very happy to receive guidance/feedback on the particulars of the UI/UX.
This is a really good RFC. Love it!
Could this possibly be extended to make it more dynamic? Why limit ourselves to just a single type.
Maybe implement some sort of base class for the main logic (most of this is already in the streamblock); subclass that to filter out the item(s) on the javascript and use the unionblock class to achieve the same on the python side. This would then still allow a list of allowed types based on some arbitrary filter method instead of just a single type. It could be a list of one [type].
In this case - the "chooser" (radio, select, whatever); would "tell" the javascript adapter which type is allowed; the js definition (the subclass I mentioned) would then filter out to only allow that block type.
This I think would also keep work minimal; as most of the logic for this would then already be implemented in the streamfield/streamblock.
Keeps it extendable for developers :wink:
As for the choicefield; my input is likely not really important - that's the UX guy's job, but I say no.
Something which aims to implement more use cases for a block should not then limit itself by the amount of blocktypes to be chosen.
What if you have 10 choices? I would not want 10 different select boxes.
Relevant slack discussion for extra info
Me
It looks like it wouldnt be too hard to extend this to dynamically show/hide menu items in the streamfield menu based on for example; meta attributes (or stimulus controllers??? :wink: ) https://github.com/wagtail/wagtail/blob/cd50955b4984b156250d1382b2cc9c880779b57c/client/src/components/StreamField/blocks/StreamBlock.js#L119 I've never used react; but this seems like it's pretty doable. I remember questions for this being asked on platforms like stackoverflow before - is there any particular reason this hasn't been implented? (edited)
LB
4 hours ago Hey Nigel. Are you talking about dynamically hiding block addition options? What's your main use case? As for why a feature hasn't been implemented, that's just the nature of software and open source. It's not been a priority or no contributions to get the feature in. Nonetheless, if you have a solid idea and feel up to raising an issue to propose it - go for it. There may also already be an issue so do a quick search. You may also be interested in reading the Union Block RFC that's just gone up. https://github.com/wagtail/rfcs/pull/94
Me
< 1 minute ago I though up an example; I'm currently wanting to do something similar for a menu system. Say you have 5 different card struct blocks - they would often share the same container. To keep things uniform you would then need to define 5 different classes of those wrapper blocks - you would not want 2 different types of card next to eachother. This takes up a lot of space; and it makes everything less intuitive inside of the addition menu. Instead of that; we could possibly implement a method on the structblock definition (js) to do an arbirtrary check. In this case; it would check for the type of the first card chosen, and then only allow choices of that type. I realize I am describing the union block; but I think this can be extended. Thanks for the link; now I know where to take this idea :wink: I was more so asking for a reason because the question might already have been asked and answered - maybe it was thought of as a bad idea :stuck_out_tongue:
Hi @Nigel2392, thanks for the feedback. The functionality I'm proposing would allow for your use case, if I've understood it correctly. You would be able to define a block like this:
class CardGroupChooser(UnionBlock):
card_option_1 = ListBlock(Card1())
card_option_2 = ListBlock(Card2())
card_option_3 = ...
For each instance of CardGroupChooser in your stream field, editors would be able to choose one of the ListBlock types.
Something which aims to implement more use cases for a block should not then limit itself by the amount of blocktypes to be chosen. What if you have 10 choices? I would not want 10 different select boxes.
I have proposed making the chooser's widget customisable, so that developers are able to select a compatible widget other than the default (e.g. a Select widget).
Hi @Nigel2392, thanks for the feedback. The functionality I'm proposing would allow for your use case, if I've understood it correctly. You would be able to define a block like this:
class CardGroupChooser(UnionBlock): card_option_1 = ListBlock(Card1()) card_option_2 = ListBlock(Card2()) card_option_3 = ...For each instance of
CardGroupChooserin your stream field, editors would be able to choose one of theListBlocktypes.Something which aims to implement more use cases for a block should not then limit itself by the amount of blocktypes to be chosen. What if you have 10 choices? I would not want 10 different select boxes.
I have proposed making the chooser's widget customisable, so that developers are able to select a compatible widget other than the default (e.g. a Select widget).
I was more thinking of something like this (rough sketch, heavily simplified, implementation would vary by a lot)
class CardGroupChooser(UnionBlock):
# example for the __union_type__ input; just to make choices clear
option = blocks.ChoiceBlock(
choices=[
("happy", _("Happy")),
("sad", _("Sad")),
],
)
card_options = StreamBlock([
("card_option_1", Card1(condition="happy"])),
("card_option_2", Card2(condition="happy"])),
("card_option_2", Card2(condition="sad"])),
])
This would take away from it being a true union-like block; but would in turn give developers lots more options.
This could even be taken one step further:
class CardGroupChooser(UnionBlock):
option = blocks.ChoiceBlock(
choices=[
("happy", _("Happy")),
("sad", _("Sad")),
],
)
weather = blocks.ChoiceBlock(
choices=[
("sunny", _("Sunny")),
("rainy", _("Rainy")),
],
)
card_options = StreamBlock([
# Only show if we are happy
("card_option_1", Card1(conditions=["option.happy"])),
# Only show if the weather is rainy and we are happy
("card_option_2", Card2(conditions=["option.happy", "weather.rainy"])),
# Only show if we are sad
("card_option_3", Card3(conditions=["option.sad"])),
])
This would mean we could still get 2 different block types if the stars align.
But! you would be able to easily subclass this to create your own union block.
Thanks for the feedback @thibaudcolas! I'll address a couple of points from your parent post here.
Implement similar generic improvements to what this suggests but in a backwards-compatible way.
Work on https://github.com/wagtail/wagtail/issues/381 as a separate data type
I feel that this would be missing an opportunity to introduce a new, useful, fundamental building block that addresses a common rough corner in user code. I do think the core value here is provided by having unions present in the "stream field type system", so that developers don't have to shoe-horn data that is best modeled as a union into a sequence type.
I do agree that having a "link type" in Wagtail could be beneficial, but even if that were implemented, I would still advocate for the introduction of UnionBlock.
Are there a lot of other widespread enough use cases for this beyond links of different types?
Speaking from my own experience, I've seen other uses of StreamBlock that could benefit from being treated as UnionBlocks, e.g. lists of cards where there are multiple "types of cards" available to populate the list, or a choice from multiple types of "callout" blocks. However, I think that while links are a useful demonstration of the use case, the real value is in fleshing out the type system so that data can be modeled more accurately in stream fields.