[Answered ]-Adding additional information into each form field

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')
class Column_SingleLineTextField(BaseField):
    field_class = CustomSinglelineField
    icon = "pilcrow"
    label = "Text field (single line)"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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:

class CustomSinglelineField(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

class CustomSinglelineField(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)

class CustomMultilineField(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)

class CustomDateField(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)

class CustomDatetimeField(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)

class CustomEmailField(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)

class CustomURLField(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)

class CustomNumberField(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)

class CustomDropdownField(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)

class CustomRadioField(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)

class CustomCheckboxesField(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)

class CustomCheckboxField(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)

class CustomSinglefileField(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)

class CustomMultifileField(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')
class Column_SingleLineTextField(BaseField):
    field_class = CustomSinglelineField
    icon = "pilcrow"
    label = "Text field (single line)"

    def get_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

    def get_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')
class MultiLineTextField(BaseField):
    field_class = CustomMultilineField
    icon = "form"
    label = "Text field (multiple lines)"

    def get_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

    def get_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')
class DateField(BaseField):
    field_class = CustomDateField
    icon = "date"
    label = "Date field"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class DateTimeField(BaseField):
    field_class = CustomDatetimeField
    icon = "time"
    label = "Time field"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class EmailField(BaseField):
    field_class = CustomEmailField
    icon = "mail"
    label = "Email field"

    def get_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

    def get_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')
class URLField(BaseField):
    field_class = CustomURLField
    icon = "link"
    label = "URL field"

    def get_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

    def get_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')
class NumberField(BaseField):
    field_class = forms.DecimalField
    label = "Number field"

    def get_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

    def get_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')
class DropdownField(BaseField):
    field_class = CustomDropdownField
    icon = "arrow-down-big"
    label = "Dropdown field"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class RadioField(BaseField):
    field_class = CustomRadioField
    icon = "radio-empty"
    label = "Radio buttons"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class CheckboxesField(BaseField):
    field_class = CustomCheckboxesField
    icon = "tick-inverse"
    label = "Checkboxes"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class CheckboxField(BaseField):
    field_class = forms.BooleanField
    icon = "tick-inverse"
    label = "Checkbox field"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class SingleFileField(BaseField):
    field_class = CustomSinglefileField
    icon = "doc-full-inverse"
    label = "File field"

    def get_options(self, block_value):
        options = super().get_options(block_value)
        options.update({'width': block_value.get('width')})
        return options

    def get_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')
class MultiFileField(BaseField):
    field_class = forms.FileField
    icon = "doc-full-inverse"
    label = "Files field"

    def get_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">
        <button class="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:

<div class="field-column-id_{{ value.form.slug }}_{{ field.name }}">
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}<p class="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.

👤SidS

Leave a comment