djangotemplatetags

Django templatetag to access arbitrary form field as defined by argument


This particular block of code repeats about a hundred million times, just with different form fields, and so I want to replace all those lines with templatetags:

<label for="{{ form.building_name.id_for_label }}">Building</label>
<input name="{{ form.building_name.name }}" value="{{ form.instance.building_name }}">

So instead of writing this:

<label for="{{ form.building_name.id_for_label }}">
    <!-- plus many more lines just for this one element -->
<label for="{{ form.building_title.id_for_label }}">
    <!-- plus many more lines just for this one element -->
...

I am looking to write this:

{% someTag 'building_name' %}
{% someTag 'building_title' %}

After much mucking about, I arrived at one of the ugliest ways of writing an inclusion tag that I've ever had the misfortune of laying eyes on. The convoluted nature of accessing the field is because I found no other way of accessing an arbitrary field of the form.

The tag:

@register.inclusion_tag("main/sff.html")
def sff(form, field_name, label):
    field = form._meta.model._meta.get_field(field_name)
    return {'form': form, 'field': field, 'label' : label, 'ph' : placeholder}

sff.html:

<label for="{{ field.id_for_label }}">{{ label }}</label>
<input type="text" \
    name="{{ field.name }}" \
    value="{{ form.instance.field }}">
</div>

With all of that said and done, here's how I call it from my HTML template:

{% sff form 'building_name' 'Enter property name' %}

Browser console shows the field as holding name=building_name, but value is empty.

I've verified in the database that the form can successfully POST the values, so the name part is presumably OK to go. It's the most important part, so I am almost at my goal.

But I cannot for the life of me figure out how to access form.instance.<arbitrary_field> in a templatetag. I am using this to display the existing value that the object has, and I'm using this approach instead of accessing it through the form directly (like field.value) because there appears to be cases where the form is either cleared out or for other reasons do not contain the database-value anymore.


Solution

  • This answer appears to me to be somewhere towards the workaround/hack area, so I will gladly accept any answer that is either more elegant or more canonical, or in general less dirty than this feels.

    I can't find an easy or clean way to resolve statements like {{ form.arbitrary_field_name }}, because I don't know exactly why but it looks like it has something to do with field objects(?)

    However, there exists a way to access the value of a form's instance's field by passing string arguments -- but only if we're using ModelForm. It doesn't require terribly deep python knowledge, and advanced users may even consider it quite basic, but it's taken me hours to think to consider this on a conceptual level.

    ModelForms will have an instance of the relevant model associated to them, and a model is for the purposes of this post identical to a class. And classes have __dict__().

    Simple templatetags also enables a new way to access form fields (works with any kind of form, as far as I know), where you get to deal with them in familiar Python instead of Django-Jinja2-HTML sorcery.

    Armed with this knowledge -- because I am using a ModelForm -- here's how the final code might look like:

    @register.inclusion_tag("main/sff.html")
    def sff(form, field_name, label):
        v = form.instance.__dict__[field]
        l = get_field_id_for_label(form, field)
        f = get_field_name(form, field)
        return {'form': form, 'field_name' : f, 'label' : label, 'value' : v, 'label_id' : l}
    
    @register.simple_tag
    def get_field_name(form, field_name):
        return form[field_name].name
    
    @register.simple_tag
    def get_field_id_for_label(form, field_name):
        return form[field_name].id_for_label
    

    And now, in main/sff.html:

    <label for="{{ label_id }}">{{ label }}</label>
    <input type="text" \
        name="{{ field_name }}" \
        value="{{ value }}">
    

    And voila, the templating engine now takes this input in a template:

    {% sff form 'building_name' 'Building' %}

    and renders it as this for the browser:

        <label for="id_building_name">Building</label>
        <input type="text" \
            class="form-control" \
            name="building_name" \
            value="ASDF">
    

    Which works completely fine in all respects mentioned in the question.