怎么样在Django里实现联动下拉列表选项?
从属或链接下拉列表是一个特殊字段,它依赖于先前选择的字段,以显示已过滤选项的列表。最常见的地方就是通过地址里选择省份,然后显示出该省份下所有城市列表。
以下面的应用程序为例:
models.py
from django.db import models class Country(models.Model): name = models.CharField(max_length=30) def __str__(self): return self.name class City(models.Model): country = models.ForeignKey(Country, on_delete=models.CASCADE) name = models.CharField(max_length=30) def __str__(self): return self.name class Person(models.Model): name = models.CharField(max_length=100) birthdate = models.DateField(null=True, blank=True) country = models.ForeignKey(Country, on_delete=models.SET_NULL, null=True) city = models.ForeignKey(City, on_delete=models.SET_NULL, null=True) def __str__(self): return self.name
在应用程序中,我们将创建一个简单的表单处理来创建和更新人物对象。这个联动列表用来选择人物的国家和所在城市。
urls.py
from django.urls import include, path from . import views urlpatterns = [ path('', views.PersonListView.as_view(), name='person_changelist'), path('add/', views.PersonCreateView.as_view(), name='person_add'), path('<int:pk>/', views.PersonUpdateView.as_view(), name='person_change'), ]
创建三个视图函数:
views.py
from django.views.generic import ListView, CreateView, UpdateView from django.urls import reverse_lazy from .models import Person class PersonListView(ListView): model = Person context_object_name = 'people' class PersonCreateView(CreateView): model = Person fields = ('name', 'birthdate', 'country', 'city') success_url = reverse_lazy('person_changelist') class PersonUpdateView(UpdateView): model = Person fields = ('name', 'birthdate', 'country', 'city') success_url = reverse_lazy('person_changelist')
我们运行它,但是发现这个功能和我们想要要的不太一样,我们是想要根据选择国家,然后显示出该国家下的所有城市。但现在程序把所有国家的城市都给列出来了。
用一个简单的HTML代码把效果显示出来:
{% extends 'base.html' %} {% block content %} <h2>Person Form</h2> <form method="post" novalidate> {% csrf_token %} <table> {{ form.as_table }} </table> <button type="submit">Save</button> <a href="{% url 'person_changelist' %}">Nevermind</a> </form> {% endblock %}
实现它的最佳方法是创建一个模型表单。通过这种方式,我们可以非常灵活地实现我们想要的功能。
forms.py
from django import forms from .models import Person, City class PersonForm(forms.ModelForm): class Meta: model = Person fields = ('name', 'birthdate', 'country', 'city') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['city'].queryset = City.objects.none()
上面的示例有一个比较重要细的地方,我们简单的定义表单:现我们将覆盖默认的init方法,并将city字段的queryset设置为空的列表:
PS:不要忘记修改视图定义,使用我们的新表单类:
views.py
class PersonCreateView(CreateView): model = Person form_class = PersonForm success_url = reverse_lazy('person_changelist') class PersonUpdateView(UpdateView): model = Person form_class = PersonForm success_url = reverse_lazy('person_changelist')
现在我们需要创建一个视图来返回给定国家/地区的城市列表。该视图将通过使用AJAX进行请求。
views.py
def load_cities(request): country_id = request.GET.get('country') cities = City.objects.filter(country_id=country_id).order_by('name') return render(request, 'hr/city_dropdown_list_options.html', {'cities': cities})
用这样简单的基于函数的视图就可以非常好的实现。下面是我们的HTML模板:
templates/hr/city_dropdown_list_options.html
<option value="">---------</option> {% for city in cities %} <option value="{{ city.pk }}">{{ city.name }}</option> {% endfor %}
然后我们给之前的视图函数创建一个URL:
urls.py
from django.urls import include, path from . import views urlpatterns = [ path('', views.PersonListView.as_view(), name='person_changelist'), path('add/', views.PersonCreateView.as_view(), name='person_add'), path('<int:pk>/', views.PersonUpdateView.as_view(), name='person_change'), path('ajax/load-cities/', views.load_cities, name='ajax_load_cities'), # <--新增这个 ]
现在建一个AJAX请求。在下面的示例中,我使用的是jQuery,但可以使用任何JavaScript框架(或只是简单的JavaScript)来创建异步请求:
templates/person_form.html
{% extends 'base.html' %} {% block content %} <h2>Person Form</h2> <form method="post" id="personForm" data-cities-url="{% url 'ajax_load_cities' %}" novalidate> {% csrf_token %} <table> {{ form.as_table }} </table> <button type="submit">Save</button> <a href="{% url 'person_changelist' %}">Nevermind</a> </form> <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script> <script> $("#id_country").change(function () { var url = $("#personForm").attr("data-cities-url"); // 获取 `load_cities` view var countryId = $(this).val(); // 在HTML中获取选取的国家ID $.ajax({ // 初始化 AJAX 请求 url: url, //设置请求地址(= localhost:8000/hr/ajax/load-cities/) data: { 'country': countryId // countryId传到GET参数 }, success: function (data) { // 视图函数 `load_cities`返回到`data` $("#id_city").html(data); // 用返回的数据替换城市输入框的内容 } }); }); </script> {% endblock %}
首先,我为表单(personForm)添加了一个ID,以便我们可以更容易地访问它。之后,我将数据属性添加到专用章data-cities-url。
然后,在此之后,我们在国家/地区下拉列表中有一个监听器,设置id_country标识。此ID由Django自动生成。当它发生变化时,它会向服务器发出一个AJAX请求,并将所选的国家ID传递给我们的视图。
请求成功后,我们的脚本将在cities下拉列表中添加由load_cities视图呈现的HTML,由HTML ID id_city标识。
现在前端已经做好了,但是后端并没有按预期工作。如果我们现在提交表单,我们将看到以下错误消息:
那是因为我们在表单定义中列出了空的城市列表。这个错误消息是我特意向你显示的,因为它实际上非常有用。它将帮助我们保持形式的一致性。这意味着Django表单检查查询集中是否存在提供的值。
修改一下代码:
forms.py
from django import forms from .models import Person, City class PersonForm(forms.ModelForm): class Meta: model = Person fields = ('name', 'birthdate', 'country', 'city') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['city'].queryset = City.objects.none() if 'country' in self.data: try: country_id = int(self.data.get('country')) self.fields['city'].queryset = City.objects.filter(country_id=country_id).order_by('name') except (ValueError, TypeError): pass # invalid input from the client; ignore and fallback to empty City queryset elif self.instance.pk: self.fields['city'].queryset = self.instance.country.city_set.order_by('name')
这个表单将提供一个非常好的行为。如果有表单POST数据(数据不是无),它将使用收到的表单的国家ID加载城市列表。如果它是无效输入,只需丢弃它,表单将为用户提示错误信息。如果没有POST数据但表单中有实例(意味着正在使用该表单更新现有人员),请使用所选国家/地区的城市列表。如果没有,只需返回一个空的城市列表,因为它是一个全新的形式(self.fields ['city']已经设置为空的查询集,还记得吗?)。
或者,您可以从表单定义中完全删除country字段,因为它与城市无关。但我更喜欢将它保留在那里,因为在某些情况下您需要保存这两个值,并且在上面的示例中,您可以了解如何实现它而不必干扰表单的呈现过程。
最好的学习方法是自己尝试。检查GitHub上的代码并在本地尝试。修改这个例子,把它变成你的。如果您有任何疑问,请在下面的评论中留言!
本文例子效果:dependent-dropdown-example.herokuapp.com
代码:github.com/sibtc/dependent-dropdown-example/
这里一个已经写好的插件,主要是在DjangoAdmin里使用。下载地址:https://github.com/digi604/django-smart-selects
文章评论 2
第一个例子怎么实现的,form 没地方传输啊
请教下,正好有个类似的demo,前面字段选择后后面字段的内容联动更新,这个参考楼主的内容后已完成,但我想要的是后面字段的内容实现多选。实现多选的时候呢样式上就是一大个框,如果用了select多选的样式呢,下拉框内的选项又无法实现代码中的联动选择。请问能否提供一下思路呢, 谢谢!