0

I need to implement two dropdown lists that the values of the seconds depends on the selection of the first.

I was able to implement that in the backend but I am struggling to do it in the front end and more specifically with javascript!

countries = Country.objects.filter(Enabled=True)
citiesByCountry = {}

for country in countries:
    citiesInCountry = City.objects.filter(Enabled=True, Country=country)
    cities = []

    for city in citiesInCountry:
        cities.append(city.Name)

    citiesByCountry[country.Name] = cities

context = {'citiesByCountry': citiesByCountry}
return render(request, 'index.html', context)

So I have the following structure:

'Country':['City1', 'City2']

Here is the HTML:

<div class="form-group col-md-4">
    <select class="form-control" onchange="test(this.value)" id="sel1">
        {% for country in citiesByCountry %}
            <option value="{{ country }}">{{ country }}</option>
        {% endfor %}
    </select>
</div>
<div class="form-group col-md-4">
    <select class="form-control" id="cities">
    </select>
</div>

So I have added the following javascript:

<script>
    var country_objs = {};
    {% for country, cities in citiesByCountry.items %}
        country_objs['{{country|escapejs}}'] = '{{cities|escapejs}}';
    {% endfor %}
</script>

<script type="application/javascript">
    function test(country) {
        var $cities_select = $("#cities");
        $(country_objs[country]).each(function(){
            $cities_select.append('<option>' + this + '<\option>');
        });
    }
</script>

The second dropdown never get populated but when I print the contents of the country_objs like this: console.log(country_objs[country]);

I get the following:

['City1', 'City2', 'City3']

Which is correct, but the .each function does not loop through the items. I think the problem is that the above is not a proper array but a string but still can't understand why.

Note that I get the following error:

jquery.min.js:2 Uncaught Error: Syntax error, unrecognized expression: ['City1', 'City2', 'City3']

Unfortunately whatever I try won't work, I couldn't imagine that implementing this in Django will be so hard.

I would like to avoid using a third-party app or module to do this simple thing and I would like to use a proper way to do it (i.e the best way) so any ideas will be really valuable.

Phrixus
  • 1,209
  • 2
  • 19
  • 36

2 Answers2

1

There are two solutions:

Solution 1:

use a for loop:

country_objs['{{country|escapejs}}'] = [{% for city in cities %}"city",{% endfor %}];

Solution 2:

Switch the line:

citiesByCountry[country.Name] = cities

for:

citiesByCountry[country.Name] = json.dumps(cities)

to encode to json, and then in the template:

country_objs['{{country|escapejs}}'] = {{cities|safe}};

Obs regarding solution 2:

You can't have the single quotes around the variable

'{{cities|safe}}';

in the second solution, or else when you add the list ['City1', 'City2', 'City3'] you're gonna have:

'['City1', 'City2', 'City3']'
Fabio
  • 3,015
  • 2
  • 29
  • 49
  • Worked!! Thank you! The only problem is that I my cities dropdown has some empty strings as options among the actual cities which I am not sure where they came from. – Phrixus Jul 11 '16 at 17:02
  • no prob :) If you print the list in python there are no empty strings? Also, don't forget to set the value of the optinos to the cities as well – Fabio Jul 11 '16 at 17:07
  • No there are no spaces in python list and I set the value as well. – Phrixus Jul 11 '16 at 17:14
  • I found the problem. the closing `` was `<\option>` – Phrixus Jul 11 '16 at 18:00
0

I think you want to remove the |escapejs filter for the part you want to be parsed in JavaScript. You might even find you need |safe, but you should be certain that you have control over what gets output there before considering that.

var country_objs = {};
{% for country, cities in citiesByCountry.items %}
country_objs['{{country|escapejs}}'] = {{cities|safe}};
{% endfor %}

For the updating part, this should work:

function updateCities(country) {
    var $cities_select = $("#cities");
    $(country_objs[country]).each(function(key, value) {   
        $('#cities').append($("<option></option>")
                    .attr("value",key)
                    .text(value)); 
    });
}

$('#sel1').change(function() {
    updateCities(this.value);
});

Credit due in part to this answer https://stackoverflow.com/a/171007/823020.

The above is missing an initial setting, which you could either do in templating or JavaScript. For JavaScript, you could insert another updateCities($('#cities).val());.

The above also appends every time, instead of resetting the options to an empty list. This is left as an exercise for the reader.

Suggestion 1: You didn't discuss this, but your initial query would be better done something like this:

# the following may differ based on your foreign key's related_name attribute
countries = Country.objects.filter(Enabled=True).select_related('city_set')
for country in countries:
    citiesInCountry = country.city_set.values_list('name', flat=True)

This would all be a single query. However you'd need to rethink about the 'active' flag, or how to achieve that if you still need it.

Suggestion 2: To be honest, it might be better in general to wrap it up in json. In your view:

import json
countries_json = json.dumps(citiesByCountry)

and then in the template:

var country_objs = {{ citiesByCountry|safe }};
Community
  • 1
  • 1
nimasmi
  • 3,978
  • 1
  • 25
  • 41