Search code examples
htmldjangobootstrap-4django-formsdjango-crispy-forms

Render django's Multiwidget & MutliValueField Textarea using Crispy Forms


I have a TextField intended to store a large amount of text that can be logically split into 10 parts. I thought that it would make sense to create 10 separate Textareas, each for one logical part. Thus, I subclassed MultiWidget and MultiValueField like so:

class MultiWidget(forms.widgets.MultiWidget):
    template_name = "custom_content_widget.html"
    attrs = {"class": "textarea form-control"}

    def __init__(self, attrs=None):
        widgets = [Textarea()] * 10
        super(MultiWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return value
        return ["", "", "", "", "", "", "", "", "", ""]


class ContentField(MultiValueField):
    widget = MultiWidget

    def __init__(self, *args, **kwargs):
        # Define one message for all fields.
        error_messages = {
            'required': 'This field is required.',
        }
        # Or define a different message for each field.
        fields = [CharField()] * 10

        super(ContentField, self).__init__(
            error_messages=error_messages, fields=fields, require_all_fields=True, *args, **kwargs)

        # self.helper = FormHelper()
        # self.helper.layout = Layout(
        #
        # )

    def compress(self, data_list):
        return " ".join(data_list)

with the custom_content_widget.html being just

{% for subwidget in widget.subwidgets %}
    {% with widget=subwidget %}
        {% include widget.template_name %}
    {% endwith %}
{% endfor %}

Simple model and form in which I'd like to use this multiwidget

class Opinion(models.Model):
    content = models.TextField()

class OpinionForm(forms.ModelForm):
    content = ContentField()

    class Meta:
        model = Opinion
        fields = ('__all__')

The problem is that when I use content in my form's HMTL as {{ form.content | as_crispy_field }} it renders pretty ugly enter image description here and I'd like all of the Textareas to be rendered one under the other. The main issue here is that textarea is rendered as

<textarea name="content_0" cols="40" rows="10" class="textarea" required id="id_content_0">
</textarea>

while "normal" TextField is rendered as

<textarea name="content" cols="40" rows="10" class="textarea form-control" required id="id_content">
</textarea>

and I have no clue how could I force the class of the widget to be textarea form-control instead of textarea. Initially, I found this question and also this blog but all they do is just to properly group widgets into rows and columns. Is there anything I am missing here?


Solution

  • The key was to pass attributes dict with "class": "textarea form-control" to the MultiWidget constructor as follows

    class ContentField(MultiValueField):
        widget = MultiWidget({"class": "textarea form-control"})
    
        def __init__(self, *args, **kwargs):
            # Define one message for all fields.
            error_messages = {
                'required': 'This field is required.',
            }
            # Or define a different message for each field.
            fields = [CharField()] * 10
    
            super(ContentField, self).__init__(
                error_messages=error_messages, fields=fields, require_all_fields=True, *args, **kwargs)
    
        def compress(self, data_list):
            return " ".join(data_list)