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.