Skip to content Skip to sidebar Skip to footer

Adding Additional Information Into Each Form Field

I am using Django/Wagtail and the wagtailstreamforms package to build forms. Unfortunately you cannot add two fields side-by-side in the wagtailstreamforms form builder out of the

Solution 1:

My crude but effective solution

As I was unable to override the CharField model to add the additional parameter, width, I thought it would be easier to utilise an already existing parameter to pass the width value into the templates. For this approach, I will be using the attrs parameter to pass the width value into the HTML rendered instance of each form field, I will then use some jQuery to extract the value and add it to the class of the container div.

Starting with the singleline CharField, we need to override the field to allow the user to input a width value, my field override class looks like this:

@register('singleline')classColumn_SingleLineTextField(BaseField):
    field_class = CustomSinglelineField
    icon = "pilcrow"
    label = "Text field (single line)"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

Now we need to override the CharField class to set the width value as one of the attributes in attrs:

classCustomSinglelineField(forms.CharField):def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.TextInput(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

The whole code (wagtailstreamforms_fields.py)

I have also added a placeholder option to some of the applicable fields, a rows attribute option for the multiline field, as well as the width option:

from django import forms
from wagtailstreamforms.fields import BaseField, register
from wagtail.core import blocks

classCustomSinglelineField(forms.CharField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.placeholder = kwargs.pop('placeholder')
        self.widget = forms.widgets.TextInput(attrs={'col_width': self.width, 'placeholder': self.placeholder})
        super().__init__(*args,**kwargs)

classCustomMultilineField(forms.CharField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.rows = kwargs.pop('rows')
        self.placeholder = kwargs.pop('placeholder')
        self.widget = forms.widgets.Textarea(attrs={'col_width': self.width, 'rows': self.rows, 'placeholder': self.placeholder})
        super().__init__(*args,**kwargs)

classCustomDateField(forms.DateField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.DateInput(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomDatetimeField(forms.DateField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.DateTimeInput(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomEmailField(forms.EmailField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.placeholder = kwargs.pop('placeholder')
        self.widget = forms.widgets.EmailInput(attrs={'col_width': self.width, 'placeholder': self.placeholder})
        super().__init__(*args,**kwargs)

classCustomURLField(forms.URLField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.placeholder = kwargs.pop('placeholder')
        self.widget = forms.widgets.URLInput(attrs={'col_width': self.width, 'placeholder': self.placeholder})
        super().__init__(*args,**kwargs)

classCustomNumberField(forms.DecimalField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.placeholder = kwargs.pop('placeholder')
        self.widget = forms.widgets.URLInput(attrs={'col_width': self.width, 'placeholder': self.placeholder})
        super().__init__(*args,**kwargs)

classCustomDropdownField(forms.ChoiceField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.Select(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomRadioField(forms.ChoiceField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.RadioSelect(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomCheckboxesField(forms.MultipleChoiceField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.SelectMultiple(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomCheckboxField(forms.BooleanField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.CheckboxInput(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomSinglefileField(forms.FileField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.ClearableFileInput(attrs={'col_width': self.width})
        super().__init__(*args,**kwargs)

classCustomMultifileField(forms.FileField):
    def__init__(self,*args,**kwargs):
        self.width = kwargs.pop('width')
        self.widget = forms.widgets.ClearableFileInput(attrs={'col_width': self.width,"multiple": True})
        super().__init__(*args,**kwargs)


@register('singleline')classColumn_SingleLineTextField(BaseField):
    field_class = CustomSinglelineField
    icon = "pilcrow"
    label = "Text field (single line)"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'placeholder': block_value.get('placeholder')})
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('placeholder', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('multiline')classMultiLineTextField(BaseField):
    field_class = CustomMultilineField
    icon = "form"
    label = "Text field (multiple lines)"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        options.update({'rows': block_value.get('rows')})
        options.update({'placeholder': block_value.get('placeholder')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('placeholder', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ('rows', blocks.IntegerBlock(help_text="Height of field", max_value=100, min_value=1, default=10, required=True)),
        ], icon=self.icon, label=self.label)

@register('date')classDateField(BaseField):
    field_class = CustomDateField
    icon = "date"
    label = "Date field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('datetime')classDateTimeField(BaseField):
    field_class = CustomDatetimeField
    icon = "time"
    label = "Time field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('email')classEmailField(BaseField):
    field_class = CustomEmailField
    icon = "mail"
    label = "Email field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        options.update({'placeholder': block_value.get('placeholder')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('placeholder', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('url')classURLField(BaseField):
    field_class = CustomURLField
    icon = "link"
    label = "URL field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        options.update({'placeholder': block_value.get('placeholder')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('placeholder', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('number')classNumberField(BaseField):
    field_class = forms.DecimalField
    label = "Number field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        options.update({'placeholder': block_value.get('placeholder')})
        return options

    defget_form_block(self):
        return blocks.StructBlock([
            ('label', blocks.CharBlock()),
            ('help_text', blocks.CharBlock(required=False)),
            ('required', blocks.BooleanBlock(required=False)),
            ('default_value', blocks.CharBlock(required=False)),
            ('placeholder', blocks.CharBlock(required=False)),
            ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
        ], icon=self.icon, label=self.label)

@register('dropdown')classDropdownField(BaseField):
    field_class = CustomDropdownField
    icon = "arrow-down-big"
    label = "Dropdown field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ("empty_label", blocks.CharBlock(required=False)),
                ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

@register('radio')classRadioField(BaseField):
    field_class = CustomRadioField
    icon = "radio-empty"
    label = "Radio buttons"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

@register('checkboxes')classCheckboxesField(BaseField):
    field_class = CustomCheckboxesField
    icon = "tick-inverse"
    label = "Checkboxes"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ("choices", blocks.ListBlock(blocks.CharBlock(label="Option"))),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

@register('checkbox')classCheckboxField(BaseField):
    field_class = forms.BooleanField
    icon = "tick-inverse"
    label = "Checkbox field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

@register('singlefile')classSingleFileField(BaseField):
    field_class = CustomSinglefileField
    icon = "doc-full-inverse"
    label = "File field"defget_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

@register('multifile')classMultiFileField(BaseField):
    field_class = forms.FileField
    icon = "doc-full-inverse"
    label = "Files field"defget_form_block(self):
        return blocks.StructBlock(
            [
                ("label", blocks.CharBlock()),
                ("help_text", blocks.CharBlock(required=False)),
                ("required", blocks.BooleanBlock(required=False)),
                ('width', blocks.IntegerBlock(help_text="Width of field, 1 to 12, 12 is full width.", max_value=12, min_value=1, default=12, required=True)),
            ],
            icon=self.icon,
            label=self.label,
        )

Now the templates

The templates/custom_form_block.html is unchanged and looks like the following:

<form{% if form.is_multipart %} enctype="multipart/form-data"{% endif %} class="row g-3 normal-form" action="{{ value.form_action }}" method="post" novalidate>
    {{ form.media }}
    {% csrf_token %}
    {% for hidden in form.hidden_fields %}{{ hidden }}{% endfor %}
    {% for field in form.visible_fields %}
        {% include 'partials/custom_form_field.html' %}
    {% endfor %}
    <div class="col-12">
        <buttonclass="btn btn-primary">{{ value.form.submit_button_text }}</button>
    </div>
</form>

And the templates/partials/custom_form_field.html has all the changes and looks like the following:

<divclass="field-column-id_{{ value.form.slug }}_{{ field.name }}">
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}<pclass="help-text">{{ field.help_text }}</p>{% endif %}
    {{ field.errors }}
</div><script>var col_width = $(".field-column-id_{{ value.form.slug }}_{{ field.name }} #id_{{ field.name }}").attr("col_width")
    $(".field-column-id_{{ value.form.slug }}_{{ field.name }}").addClass("col-"+col_width);
    
    // Set the "None" placeholders as ""var placeholder = $(".field-column-id_{{ value.form.slug }}_{{ field.name }} #id_{{ field.name }}").attr("placeholder")
    if (placeholder == "None") {
        $(".field-column-id_{{ value.form.slug }}_{{ field.name }} #id_{{ field.name }}").attr("placeholder", "")
    }
</script>

I am setting the parent div for each field as field-column-id_{{ value.form.slug }}_{{ field.name }}, this way if we have multiple forms with fields of the same name on the same page, there wouldn't be any conflicts as each form will have a unique slug.

The jQuery retrieved the col_width HTML attribute that we set as the width option and will append it to col- and set it as the parent div's class.

By default, if we don't set a value in the 'placeholder' option, wagtailstreamforms sets this option as 'None'. We don't want an empty placeholder to be displayed as 'None' in the HTML so we have a jQuery script that replaces None with an empty string. This will work for the most part but this functionality breaks when the user wants the placeholder to actually be set to None.

This solution works for the most part, but in my honest opinion, I'm not very proud of it as I think this is inefficient and hacky.

Post a Comment for "Adding Additional Information Into Each Form Field"