ChoiceField comparison not working in template

I have this form:

class SearchForm(Form):
    scope = ChoiceField(
        choices=[(0, "All"), (1, "Print"), (2, "Digital"), (3, "Other")],
        initial=0,
        widget=HiddenInput(),
        required=True,
    )

The HTML component looks like this:

{% for scope_id, scope in search_form.fields.scope.choices %}
    <li class="nav-item" role="presentation">
        <button @click="$refs.scope.value = '{{ scope_id }}'; $refs.form.submit()"
                class="nav-link {% if scope_id == search_form.scope.value %}active{% endif %}"
                type="button" role="tab">
            DEBUG: {{ scope }} | {{ scope_id }} | {{ search_form.scope.value }}
        </button>
    </li>
{% endfor %}

...

<form method="post">
...
{{ search_form.scope|attr:"x-ref:scope" }}
...
</form>

The component is not within the form, but uses alpinejs to update the actual hidden widget that is in the form, and then submits it. This works without issues.

The problem I have is the if-condition breaking. While the form is unbound the condition scope_id == search_form.scope.value is True and the text shows DEBUG: All | 0 | 0

After clicking on any of the other choices, and after the form is submitted, I can confirm that the website shows DEBUG: Print | 1 | 1, which means that {% if scope_id == search_form.scope.value %} (or 1 == 1) should be True, but it isn’t. Why?

If you are rendering this HTML fragment in Django, keep in mind that all template rendering occurs in the server and not in the browser.

Your {% if ...%} block is evaluated by Django at the time the template is being rendered. By the time it has been sent to the browser, that tag has been resolved and does not exist in the rendered html.

I suggest you examine the rendered html in your browser’s developer tools to see what has been rendered and is being processed in the browser.

Hi Ken, I am aware the rendering happens on the server. I don’t understand the rest of your comment. Did you read the code correctly?

Edit: Not trying to be confrontational, I just don’t understand what direction that response is going, or how it applies to my code. Thanks for your help!

Is the block of text that you have labeled an “HTML component” being rendered as a Django template by Django?

If so, look at your actual HTML that exists in your browser that was retrieved from the view that renders it.

Yes, it is part of my Django template.

Yes, I did that. I did include that in my question:

By text I mean that part of the template where it is written: DEBUG: {{ scope }} | {{ scope_id }} | {{ search_form.scope.value }}. It’s all rendered on the server. So if this results in DEBUG: Print | 1 | 1, why is {% if scope_id == search_form.scope.value %} then False?

Please post the complete block of html - not just the DEBUG excerpt.

This is the complete block of code.

This is what you’re saying you are rendering.

What we need to see is what this looks like in the browser as shown by your browser’s developer tools, after it has been rendered by Django.

With the unbound form:

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '0'; $refs.form.submit()" class="nav-link active" type="button" role="tab">
        DEBUG: All | 0 | 0
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '1'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Print | 1 | 0
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '2'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Digital | 2 | 0
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '3'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Other | 3 | 0
    </button>
</li>

And with the bound form where scope = 1. Notice that the .active class is not present for the “Print” item.

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '0'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: All | 0 | 1
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '1'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Print | 1 | 1
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '2'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Digital | 2 | 1
    </button>
</li>

<li class="nav-item" role="presentation">
    <button @click="$refs.scope.value = '3'; $refs.form.submit()" class="nav-link " type="button" role="tab">
        DEBUG: Other | 3 | 1
    </button>
</li>

What does the view look like that is processing all this?

Actually, I’m going to guess that this is a data-type issue. Your choices are defined with integer values, but I’m going to guess at some point along this chain, something is converting that to a string - and so the comparison is failing.

It’s massive and I cannot post the entire code. These are the relevant parts:

@login_required
def abonnent_list(request):
    ...
    search_form = SearchForm(request.POST or None)
    ....
 if search_form.is_valid():
        scope = search_form.cleaned_data["scope"]
    else:
        scope = search_form.get_initial_for_field(search_form.fields["scope"], "scope")

 return render(
        request,
        "abonnenten/list.html",
        {
            "search_form": search_form,
        },
    )

I think that using a TypedChoiceField with coerce=int (see Form fields | Django documentation | Django) instead of a ChoiceField would solve the problem. Currently, the value of field is normalized to a string (as stated in Form fields | Django documentation | Django).

@antoinehumbert After some digging I found that you are right, the issue comes from types not matching. The TypedChoiceField does not solve the problem, however. I believe this is a bug and reported it here: #34721 (ChoiceField/TypedChoiceField: .value() has inconsistent behaviour, coercion not applied.) – Django

I don’t believe it is a bug.

The only attribute of the data in a bound form that is supposed to be normalized to the proper type is cleaned_data. All data submitted (bound) to the form coming from the client must be strings. Form POST data doesn’t have any other type. Note that POST data doesn’t quote strings, it’s submitted as name=value. When Django receives it, it can’t do anything other than accept it as a string.

(And yes, I did read your submitted ticket.)

Looking at the BoundField code, it appears that the BoundField value is the initial value (the 0 int in your case) if the form is not bound, or the value from POST data (which is always a string) when form is bound.

So the comparison with integers (from the choices) works when form is not bound, but cannot work on a bound form.

If you want the comparison to work in all cases, you have to convert values (from choices and bound field value) to str before doing comparison. E.g.

{% if scope_id|stringformat:"s" == search_form.scope.value|stringformat:"s" %}

It does not seem very straight forward, but I think this is the way BoundFields work.

Using TypedChoiceField does not help here, even if I think this is still relevant to retrieve the correct type for scope in your view after form validation.

Yeah, I learned that, too. Value is holding the raw input, which is why it is a string. I’m using a form to manage a table with search, filtering, more filtering and pagination, because that’s the only way of passing multiple parameters in a request, without adding dozens of lines of complex code to my view and template. But that also means that I also need the data of invalid form. If the search query does not meet the minimum of 3 chars, I still want the user to go to the next page. Hence why I need the value.

Overall, these kind of scenarios feel extremely complicated to implement, but on the other hand are nothing you don’t see on any other web page as well. I wish Django would provide some proper way of handling more dynamic pages. Ugh

Superficially, I dare say that there are easier ways to implement what you’re trying to do if you believe that

is your only solution.

The combination of Django with JavaScript is extremely powerful, and I’ve never found what you’ve stated above to be true. The ultimate solution may not be immediately obvious, but I’ve always found one.

What approach would you recommend for such a use-case?

It really depends upon the specific situation.

I don’t understand what the requirements are for this particular issue you’re trying to address, so there’s no way for me to provide a detailed answer.

I have a view that lists data in a table.

The user should be able to sort the data by field (sort=fieldname, order=asc) and filter by two different fields (color=green/blue/…, shape=…) and go through pages (page=). And the user should be able to search data in the table (query=…).

I was trying to work with a form. The form should validate some things like the query having min. 3 chars. But entering 2 chars in the query should not present the user from changing page. (Which is why I accessed the form…value fields and not cleaned-data, that is only available on valid forms).

I found forma also to be less ideal, because I don’t want to wrap the entire page into a form, the query being at the top of the page and the paginatior at the bottom.