Skip to content

Instantly share code, notes, and snippets.

@irugoy
Last active July 23, 2025 11:45
Show Gist options
  • Save irugoy/31980fbb0f56940558d282cc00f40750 to your computer and use it in GitHub Desktop.
Save irugoy/31980fbb0f56940558d282cc00f40750 to your computer and use it in GitHub Desktop.

Description

While developing a new feature usually we feel the need of having kind of a "Settings" section at block Level inside the Streamfield, this is to avoid facing the Editors to lots of fields that are not really for content just block-configuration.

We usually have a bunch of "settings" fields that are inherited across multiple blocks and it is kind of crazy that as soon as you add a new block to a streamfield the first fields you see are the non-content related and are repeated many times. It would be very nice to have a "cog" icon (next to the "duplicate" icon) that once it gets clicked it opens a section/modal/slide-in where those block-settings fields values can be defined.

Until now we've used it for:

  • Block Theme dropdown
  • Block Vertical Padding
  • Block Width (full, boxed and so)
  • Carrousel Block settings
  • Video Player Block settings
  • Statement Block settings like text alignment
  • Image & Text Block (image size and position)

Currently we've done this in a very rawly way, it is by adding an extra css class called is-block-setting to the block field attributes and then intervene the struct_block base admin template. This way while defining our block-class we also easily define which fields should be considered/grouped as settings without writing any extra code/class.

Wagtail Reference

https://github.com/wagtail/wagtail/issues/2299

Current Implementation

Screenshots

https://user-images.githubusercontent.com/1500627/159998083-982a7f42-c986-4784-bbad-340e7c018e4b.png https://user-images.githubusercontent.com/1500627/159998093-dd912278-886d-4e63-8a3b-8b0876694f59.png https://user-images.githubusercontent.com/1500627/160113342-dddc09ca-f9f1-4650-9cf1-6d69ab450230.png

Code

Here are main files involved in our implementation.

from wagtail.blocks import StructBlock, ChoiceBlock, PageChooserBlock, CharBlock, RichTextBlock
from django.utils.translation import gettext_lazy as _
from . import choices as default_choices
class RootBlock(StructBlock):
class Meta:
form_template = 'struct_block.html'
class ThemedBlock(RootBlock):
theme = ChoiceBlock(label=_('Theme'), choices=default_choices.BLOCK_THEMES, default='white',
form_classname='is-block-setting col-6')
class PaddingManagedBlock(RootBlock):
padding_top = ChoiceBlock(label=_('Padding Top'), choices=default_choices.BLOCK_TOP_PADDINGS, default='pt-100',
form_classname='is-block-setting col-6', required=False)
padding_bottom = ChoiceBlock(label=_('Padding Bottom'), choices=default_choices.BLOCK_BOTTOM_PADDINGS, default='pb-70',
form_classname='is-block-setting col-6', required=False)
class CallToActionBoxBlock(ThemedBlock, PaddingManagedBlock):
text = RichTextBlock(label=_('Text'), features=['link', 'document-link'], max_length=255, required=False)
related_page = PageChooserBlock(label=_('Related page'), required=False)
button_label = CharBlock(label=_('Button label'), max_length=255, default='Learn more', required=False)
BLOCK_THEMES = getattr(settings, 'BLOCK_THEMES', (
('white', _("White")),
('sky-blue-ligther', _('Sky Blue Lighter')),
('none', _('None')),
('default', _('Default')),
))
BLOCK_BOTTOM_PADDINGS = getattr(settings, 'BLOCK_BOTTOM_PADDINGS', (
('', _('Default')),
('big', _('Big (Standard * 1.333)')),
('medium', _('Medium (Standard / 2)')),
('small', _('Small (Standard / 3)')),
('none', _('No Margin')),
))
BLOCK_TOP_MARGINS = getattr(settings, 'BLOCK_TOP_MARGINS', (
('', _('Default')),
('big', _('Big (Standard * 1.333)')),
('medium', _('Medium (Standard / 2)')),
('small', _('Small (Standard / 3)')),
('none', _('No Margin')),
))
.struct-block-config {
display: flex;
flex-flow: column;
margin-top: 5px;
}
.struct-block-config.is-expanded {
margin-bottom: 15px;
}
.struct-block-config .config-toggler{
font-size: 10px;
margin-left: auto;
}
.struct-block-config.is-expanded .config-toggler{
border-bottom-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-bottom: 1px solid #b1dcd5 !important;
}
.struct-block-config.is-expanded .config-toggler:hover,
.struct-block-config .config-toggler:hover{
cursor: pointer;
}
.struct-block-config .config-toggler .icon{
margin-left: 5px;
}
.struct-block-config .config-toggler .icon::before{
display: inline-block;
margin-right: -1px;
width: 1em;
}
.struct-block-config .config-fields{
display: none;
background-color: #f5f5f5;
border-radius: 2px;
border: 1px solid #b1dcd5;
overflow: hidden;
}
.struct-block-config .config-fields .w-field__wrapper {
padding-bottom: 0.333rem;
}
.struct-block-config .config-fields .field {
margin-top: 0.5em;
}
.struct-block-config .config-fields input:not([type="checkbox"]),
.struct-block-config .config-fields select,
.struct-block-config .config-fields textarea {
width: 100%;
}
.struct-block-config.is-expanded .config-fields{
/* padding: 20px; */
/* margin-bottom: 2em; */
display: block;
margin-top: -1px;
display: flex;
flex-flow: row wrap;
}
.struct-block-config.is-expanded .config-fields > div {
width: 100%;
padding: 0 1rem;
}
.struct-block-config.is-expanded .config-fields > div.col-9 {
width: 75%;
}
.struct-block-config.is-expanded .config-fields > div.col-8 {
width: 66.666666%;
}
.struct-block-config.is-expanded .config-fields > div.col-6 {
width: 50%;
}
.struct-block-config.is-expanded .config-fields > div.col-4 {
width: 33.333333%;
}
.struct-block-config.is-expanded .config-fields > div.col-3 {
width: 25%;
}
.w-panel--nested .w-field__wrapper {
margin-top: -0.5em;
}
function toggleConfigClass(el) {
el.parentElement.classList.toggle('is-expanded')
return false;
}
/* Forms */
.custom-struct-block .field > label + .field {
margin-top: .25em;
}
.custom-struct-block .field > .label,
.custom-struct-block .field > label{
padding-bottom: .5em !important;
padding-top: .5em !important;
margin-bottom: -.5em;
}
.custom-struct-block .field > label + .c-sf-container {
padding: 0px 0 17px;
}
.custom-struct-block .c-sf-container .help {
margin: 8px 0 8px 0 !important;
}
.custom-struct-block .date_field .input:after,
.custom-struct-block .date_field .input:before,
.custom-struct-block .date_time_field .input:after,
.custom-struct-block .date_time_field .input:before,
.custom-struct-block .iconfield .input:after,
.custom-struct-block .iconfield .input:before,
.custom-struct-block .time_field .input:after,
.custom-struct-block .time_field .input:before,
.custom-struct-block .url_field .input:after,
.custom-struct-block .url_field .input:before {
font-size: 1.75em !important;
}
.custom-struct-block .c-sf-button {
margin: 0px 8px 8px 8px !important;
}
.custom-struct-block p, .custom-struct-block pre {
margin-top: .5em;
}
.custom-struct-block .help [class*='icon-']{
margin-right: 3px;
}
{% load i18n block_tags block_tags_private %}
<div class="{{ classname }} custom-struct-block">
{% if children|test_config_settings == 'True' %}
<div class="struct-block-config">
<button onclick="toggleConfigClass(this); return false;" class="config-toggler button button-small button-secondary">{%trans 'Settings' %}<i class="icon icon-cog" aria-hidden="true"></i></button>
<div class="config-fields fields ">
{% for child_in_config in children.values %}
{% if 'is-block-setting' in child_in_config.block.meta.form_classname %}
<div class="field {% if child.block.required %}required{% endif %} {{ child_in_config.block.meta.form_classname }}">
{% if child_in_config.block.label %}
<label {% if child_in_config.id_for_label %} for="{{ child_in_config.id_for_label }}"{% endif %}>{{ child_in_config.block.label }}:</label>
{% endif %}
{{ child_in_config.render_form }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endif %}
{% if help_text %}
<div class="sequence-member__help help"><span class="icon-help-inverse" aria-hidden="true"></span>{{ help_text }}</div>
{% endif %}
<div class="fields">
{% for child in children.values %}
{% if not child.block.meta.form_classname or 'is-block-setting' not in child.block.meta.form_classname %}
<div class="field {% if child.block.required %}required{% endif %} {{ child.block.meta.form_classname }}">
{% if child.block.label %}
<label {% if child.id_for_label %} for="{{ child.id_for_label }}"{% endif %}>{{ child.block.label }}:</label>
{% endif %}
{{ child.render_form }}
</div>
{% endif %}
{% endfor %}
</div>
</div>
# Uncomment this to enable dropdowns with icons/colors
from django.templatetags.static import static
from django.utils.html import format_html, format_html_join
from django.conf import settings
from wagtail import hooks
@hooks.register("insert_global_admin_css", order=100)
def app_blocks_global_admin_css():
"""Add /static/css/custom.css to the admin."""
return format_html('<link rel="stylesheet" href="{}">''<link rel="stylesheet" href="{}">',
static("custom_struct_block_config.css"),
static("custom_struct_block_form.css"),
)
@hooks.register("insert_global_admin_js", order=100)
def app_blocks_global_admin_js():
"""Add /static/css/custom.js to the admin."""
return format_html('<script src="{}"></script>',
static("custom_struct_block_config.js"),
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment