Django 中的 form 相关功能,即是为了解决围绕用户输入产生的表单提交的一系列问题。
从用户视角看表单提交
- 浏览器收到 HTML 中的
<form>
和各类<input>
,渲染出表单供用户填写 - 用户填写后,点提交按钮,浏览器发送 POST 请求到服务端
- 如果提交的表单数据有误,页面刷新后仍在表单页,并且显示哪些字段出错
- 如果提交的表单数据正常,页面重定向到成功后的页面
这里 Django 用了 Post/Redirect/Get 的 pattern。因为假如没有 redirect 过程且提交后的页面 URL 不变,此时用户点刷新按钮时,一些老的浏览器会重新发 POST 请求。
Django 提供的能力
官方 文档 提到 Form 体系解决了这些问题:
- preparing and restructuring data to make it ready for rendering,大体上是指字段的定义
- creating HTML forms for the data,指定义好的字段如何渲染成 HTML 表单,同时生成的表单也会做出错提示
- receiving and processing submitted forms and data from the client,指检验和清洗用户提交的数据
Django 提供了这些机制:
- Form class 和 Forms API,用来表示表单的结构化数据,同时提供框架给用户定义检验和清理逻辑
- FormView,封装了一个标准的处理用户 POST/GET 表单页面的过程
- 模板中的
form
变量,用来生成实际的 HTML 表单 - FormSet?
Form class
Basic Example
一个最简单的例子:
# forms.py
from django import forms
class NameForm(forms.Form):
your_name = forms.CharField(label='Your name', max_length=100)
类似于 Django 的 Model
类在定义数据库数据的 schema,Form
类是在定义数据的 HTML 表单中展现。Model
中的字段对应的是数据库的列,而 Form
中的字段对应的是它的 HTML 展现,即各种 <input>
标签。
当你将上面的 NameForm
生成一个空的 Form
实例传给模板引擎:
<form action="/your-name/" method="post">
{{ form }}
<input type="submit" value="Submit">
</form>
最终会被 Django 转换成 HTML 表单:
<form action="/your-name/" method="post">
<label for="your_name">Your name: </label>
<input id="your_name" type="text" name="your_name" maxlength="100">
<input type="submit" value="OK">
</form>
可以观察到:
CharField
在 HTML 上的默认展现是一个 text 类型的<input>
label
字段变成<label>
标签max_length
属性变成<input>
中的一个长度限制的属性;同时当用户提交这个字段的数据时,Django 也会对其长度做校验
这便是 Form
类所解决的核心问题。
Bound v.s. unbound
当我们将 Form
实例传到模板中时,大部分情况下这个 Form
实例都是未填充数据(unbound)的,比如说 name
字段的值为空。这是因为出现表单的场景中,大部分是我们希望用户来填。但是有一些情况,适合预先将值填充到表单中:
- 在编辑一个已有的数据时,比如作为管理员编辑帖子标题
- 有一些数据是从别的地方获取的,并不需要用户填写时
- 当用户之前在这个页面已经有提交过数据时,
- 比如用户新写了一篇帖子,点击保存后仍然希望处于编辑状态
- 比如用户上一次提交的表单数据有误时,后台在它提交后,应该返回原有的数据及相应的错误提示
这引出了 bound 和 unbound 的概念。一个 Form
实例只能处在 bound 或者 unbound 状态:
>>> f = ContactForm()
>>> f.is_bound
False
>>> f = ContactForm({'subject': 'hello'})
>>> f.is_bound
True
差异在于:
- Bound 的 form,它已经有了数据,因此可以做数据校验,同时渲染成
<input>
标签时也是带有值的 - Unbound 的 form,它还没有数据,无法做数据校验,渲染成
<input>
时值为空或者是指定的默认值
Validation
Form
提供了校验数据正确性的方法 is_valid()
,以及一系列自定义校验、清洗规则的能力,具体看 官方文档。
Widget
Form
中定义的每一种 Field
,都有对应的 Widget 类代表它的展现,比如 CharField
默认对应的是 TextInput
。你可以修改你的字段所用的是 widget 类,比如让你的 message
字段使用 Textarea
widget 而不是 TextInput
widget,这样它渲染成 HTML 时会变成 <textarea>
而不是 <input type="text">
。
The View
如果用传统的 function-based view,一个典型的表单 view 像这样:
def contact_us(request):
if request.method == 'POST':
form = forms.ContactForm(request.POST)
if form.is_valid():
form.send_mail()
return HttpResponseRedirect('/')
else:
form = forms.ContactForm()
return render(request, 'contact_form.html', {'form': form})
但 Django 提供了一个 FormView 简化了这个过程。上面的代码可以完整地换成 FormView
来这样表达:
class ContactUsView(FormView):
template_name = "contact_form.html"
form_class = forms.ContactForm
success_url = "/"
def form_valid(self, form):
form.send_mail()
return super().form_valid(form)
Template
官网文档 给出了简单易懂的说明。
在渲染成表单的 HTML 过程中,主要的问题是需要跟 CSS 框架结合,因此 Django 提供了几个能力:
- 默认的
{{ form }}
会生成仅带<label>
和<input>
的 HTML。可以用{{ form.as_table }}
{{ form.as_p }}
{{ form.as_ul }}
分别生成被<tr>
<p>
<li>
包裹的每个字段。我觉得这个功能用途一般 - 可以把每个字段的标签 ID、帮助信息和错误信息等,单独拿出来做展示。官网有文档
Forms from models
直接将 Model 用于 Form 的一种机制,省去了重复定义字段的过程:
>>> from django.forms import ModelForm
>>> from myapp.models import Article
# Create the form class.
>>> class ArticleForm(ModelForm):
... class Meta:
... model = Article
... fields = ['pub_date', 'headline', 'content', 'reporter']
# Creating a form to add an article.
>>> form = ArticleForm()
# Creating a form to change an existing article.
>>> article = Article.objects.get(pk=1)
>>> form = ArticleForm(instance=article)
Form Assets
一些 widget 往往需要额外的 CSS 和 JS 配合。Django 提供了机制让你定义这些额外依赖,并且让你方便在模板中使用它:
from django import forms
class CalendarWidget(forms.TextInput):
class Media:
css = {
'all': ('pretty.css',)
}
js = ('animations.js', 'actions.js')
>>> w = CalendarWidget()
>>> print(w.media)
<link href="http://static.example.com/pretty.css" type="text/css" media="all" rel="stylesheet">
<script type="text/javascript" src="http://static.example.com/animations.js"></script>
<script type="text/javascript" src="http://static.example.com/actions.js"></script>
你可以在模板中用 {{ form.media }}
来生成上面的三行 <link>
/ <script>
。{{ form.media }}
似乎没有官网上有明确的文档,不知道是不是 Django 团队的疏忽。
Formset
Formset 即是多个 form 组合起来的一个对象,而多个 form 意味着你可以同时在页面操作多个 model 实例。Practical Django 2 and Channels 2 中有一个例子,是实现一个购物车页面,上面有多件商品,你可以同时修改所需的数量。