django - adding attributes to fields of a MultiValueField / MultiWidget

3.6k views Asked by At

All,

I have managed to get MultiValueField and MultiValueWidget working. But, apart from "choices", I cannot seem to add attributes (like "label" or "initial") to the sub-fields that make up the MultiValueField.

Here is my (simplified) code:

class MyMultiWidget(django.forms.widgets.MultiWidget):
    def __init__(self,*args,**kwargs):
        myChoices = kwargs.pop("choices",[])
        widgets = (
            django.forms.fields.TextInput(),
            django.forms.fields.TextInput(),
            django.forms.fields.Select(choices=myChoices),
        )
        super(MyMultiWidget, self).__init__(widgets,*args,**kwargs)        

class MyMultiValueField(django.forms.fields.MultiValueField):
    widget = MyMultiWidget

    def __init__(self,*args,**kwargs):

        myLabel = "my label"
        myInitial = "my initial value"
        myChoices = [("a","a"),("b","b")]

        fields = (
            django.forms.fields.CharField(label=myLabel),
            django.forms.fields.CharField(initial=myInitial),
            django.forms.fields.ChoiceField(choices=myChoices),
        )
        super(MyMultiValueField,self).__init__(fields,*args,**kwargs)
        self.widget=MyMultiWidget(choices=myChoices)

class MyField(models.Field):

    def formfield(self,*args,**kwargs):
        return MyMultiValueField()

class MyModel(django.models.Model):
    myField = MyField(blank=True)

MyForm = modelform_factory(MyModel)

The "myField" field of MyModel is almost rendered correctly in MyForm; It shows three widgets: two TextInputs and a Select. The latter is restricted to the appropriate choices. But the former two don't have their label or initial value set.

Any suggestions on what I'm doing wrong?

Thanks.

4

There are 4 answers

0
Brian Whitton On

@george is right that the value can be defined statically, but you can also override get_context to figure out the value at run-time.

class MyInput(forms.CharField):
    def get_context(self, name, value, attrs):
        return super().get_context('some_name', 'some_value', attrs)

Will render

<input type="text" name="some_name" value="some_value" ...>
0
George On

The answer provided by ben is a bit hacky, I would pass the initial value using 'attrs':

forms.TextInput(attrs={'value': 'some text'})

Its interesting, that some of the attribute seems to be passed correctly - for example in my case setting 'max_length' on the CharField worked.

1
bjw On

You should define the format_output function of your Widget - see: https://docs.djangoproject.com/en/dev/ref/forms/widgets/

This lets you format the form html any way you like. I think the default is just to concatenate the field elements.

0
James On

I know this thread is over 9 years old but I just solved this problem myself (Python 3.9, Django 3.2).

What I ended up doing was to define an tuple of CharField objects to be passed to the MultiValueField constructor. The CharField objects were instantiated with the attributes I wanted to pass to the MultiValueField (widget including placeholder & value attributes, and help_text for each).

placeholder is a pretty effective label for each field and value can be used as the default.

Note that you can't hardcode value into the field if you're using generic class-based views on a ModelForm because it causes the field to be overwritten with the hardcoded value when opening an UpdateView. To account for this, you have to set a default value for the field in the model (I used a JSONField), then grab the relevant data from the form instance after it initialises and pass that to the field constructor in the multi_fields tuple.

EDIT: Don't forget to include the model Field in your form's Meta class. I found out the hard way that your form won't save new values if you miss this.

I then passed that tuple to the constructor as the fields argument, and used list comprehensions on the tuple to dynamically define the widget (a Custom MultiWidget constructed with the list comprehension) and help_text attributes.

# MODELS.PY
from django import models


def default_multi_values():
    return {
        'Field1': 'Field1Value',
        'Field2': 'Field2Value'
    }


class ExampleModel(models.Model):
    example_field = models.JSONField(
        default=default_multi_values()
    )
# FORMS.PY
from django import forms


class CustomMultiWidget(forms.widgets.MultiWidget):
    
    def decompress(self, value):
        **do decompress**


class CustomMultiValueField(forms.MultiValueField):
    widget = CustomMultiWidget

    def compress(self, data_list):
        **do compress**


class CustomModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        mf_values = self.instance.multi_value_field_here

        multi_fields = (
            forms.CharField(
                help_text='HelpText1',
                widget=forms.TextInput({
                    'placeholder': 'Placeholder1',
                    'value': mf_values['Field1']
                })
            ),
            forms.CharField(
                help_text='HelpText2',
                widget=forms.TextInput({
                    'placeholder': 'Placeholder2',
                    'value': mf_values['Field2']
                })
            ),
        )

        self.fields['multi_value_field_here'] = CustomMultiValueField(
            fields=multi_fields,
            widget=ColumnHeadersWidget(
                widgets=[field.widget for field in multi_fields],
            # Display an ordered list below the MultiWidget 
            # constructed from help_text messages from each consecutive field.
            help_text=
                f"<u><strong>MultiField Guide</strong></u></br>"
                f"{'</br>'.join(f'{multi_fields.index(field) + 1}: {field.help_text}' for field in multi_fields]}"
            )
        )

        class Meta:
            model = ExampleModel
            fields = [
                'example_field'
            ]