在表单实例化时动态解决表单字段参数,而不是在声明时。
项目描述
django-forms-dynamic
在表单实例化时动态解决表单字段参数,而不是在声明时。
在Python 3.6, 3.7, 3.8, 3.9和3.10上针对Django 2.2, 3.2和4.0进行了测试
安装
从PyPI安装
pip install django-forms-dynamic
用法
从视图传递参数到表单字段
改变Django表单字段的标准方式是重写表单的__init__
方法,从视图中传递任何需要的值,并在self.fields
中进行操作。
class SelectUserFromMyTeamForm(forms.Form):
user = forms.ModelChoiceField(queryset=User.objects.none())
def __init__(self, *args, **kwargs):
team = kwargs.pop("team")
super().__init__(*args, **kwargs)
self.fields["user"].queryset = User.objects.filter(team=team)
def select_user_view(request):
form = SelectUserFromMyTeamForm(team=request.user.team)
return render("form.html", {"form": form})
这可以工作,但它在更复杂的要求下扩展性不好。它也感觉有些混乱:Django表单旨在是声明式的,而这是一种非常过程式的代码。
使用django-forms-dynamic
,我们可以改进这种方法。我们需要做两件事
- 将
DynamicFormMixin
添加到你的表单类中(在forms.Form
之前)。 - 将任何需要动态行为的字段包装在
DynamicField
中。
DynamicField
构造函数的第一个参数是你所包装的字段类(例如 forms.ModelChoiceField
)。所有其他参数(除以下详细说明的特殊情况外)在创建包装字段时传递给包装字段。
但有一个非常重要的区别:通常传递给字段构造函数的任何参数都可以选择是一个可调用对象。如果它是一个可调用对象,它将在表单实例化时被调用,并传入表单实例作为参数。这个可调用对象返回的值将被传递到字段构造函数中,就像通常一样。
在我们看到代码示例之前,还有一点需要注意:在视图中,我们不是将任意的参数(如上面示例中的team
)传递给表单构造函数,而是借鉴了Django REST框架序列化器中的一个有用的惯用语,而是传递一个名为context
的单个参数,这是一个字典,可以包含您从视图中需要的任何值。这作为form.context
附加到表单上。
现在代码看起来是这样的
from dynamic_forms import DynamicField, DynamicFormMixin
class SelectUserFromMyTeamForm(DynamicFormMixin, forms.Form):
user = DynamicField(
forms.ModelChoiceField,
queryset=lambda form: User.objects.filter(team=form.context["team"]),
)
def select_user_view(request):
form = SelectUserFromMyTeamForm(context={"team": request.user.team})
return render("form.html", {"form": form})
这要漂亮多了!
使用XHR的真正动态表单
但让我们更进一步。一旦我们有了对form
的访问权限,我们可以通过根据其他字段的值来配置字段,使表单真正动态。在标准的Django请求/响应方法中,这并没有太大的意义,但当我们引入JavaScript时,这确实有意义。表单可以通过在浏览器中运行的JavaScript代码发起XHR请求从服务器多次(或分多部分)加载。
在JavaScript中从头开始实现这个功能留作读者的练习。相反,让我们看看您可能如何使用一些现代的“低JavaScript”框架来完成这个任务。
HTMX
为了说明我们将要使用的模式,我们将从HTMX文档中的一个示例中借用一个: “级联选择”。这是指一个<select>
中可用的选项取决于另一个<select>
中选择的值。请参阅HTMX文档页面以获取完整详情和示例。
我们如何使用django-forms-dynamic
实现这个后端?
首先,让我们看看表单
class MakeAndModelForm(DynamicFormMixin, forms.Form):
MAKE_CHOICES = [
("audi", "Audi"),
("toyota", "Toyota"),
("bmw", "BMW"),
]
MODEL_CHOICES = {
"audi": [
("a1", "A1"),
("a3", "A3"),
("a6", "A6"),
],
"toyota": [
("landcruiser", "Landcruiser"),
("tacoma", "Tacoma"),
("yaris", "Yaris"),
],
"bmw": [
("325i", "325i"),
("325ix", "325ix"),
("x5", "X5"),
],
}
make = forms.ChoiceField(
choices=MAKE_CHOICES,
initial="audi",
)
model = DynamicField(
forms.ChoiceField,
choices=lambda form: form.MODEL_CHOICES[form["make"].value()],
)
关键部分就在底部。我们使用一个lambda函数根据make
字段的当前选中值来加载model
字段的选项。当表单首次显示给用户时,form["make"].value()
将为"audi"
:提供给make
字段的initial
值。表单绑定后,form["make"].value()
将返回用户在make
下拉列表中选择的任何值。
HTMX倾向于鼓励将UI拆分成许多小端点,这些端点返回HTML片段。因此,我们需要两个视图:一个用于在首次页面加载时返回整个表单,另一个用于返回仅 model
字段的HTML。后者将在make
字段更改时加载,并返回所选make
的可用models
。
以下是两个视图
def htmx_form(request):
form = MakeAndModelForm()
return render(request, "htmx.html", {"form": form})
def htmx_models(request):
form = MakeAndModelForm(request.GET)
return HttpResponse(form["model"])
请记住,form["model"]
(已绑定字段)的字符串表示形式是<select>
元素的HTML,因此我们可以直接在HttpResponse
中返回它。
它们可以这样连接到URL
urlpatterns = [
path("htmx-form/", htmx_form),
path("htmx-form/models/", htmx_models),
]
最后,我们需要一个模板。我们在模板中直接使用django-widget-tweaks来添加必要的hx-
属性到make
字段。
{% load widget_tweaks %}
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.6.1"></script>
</head>
<body>
<form method="POST">
<h3>Pick a make/model</h3>
{% csrf_token %}
<div>
{{ form.make.label_tag }}
{% render_field form.make hx-get="/htmx-form/models/" hx-target="#id_model" %}
</div>
<div>
{{ form.model.label_tag }}
{{ form.model }}
</div>
</form>
</body>
</html>
Unpoly
让我们使用Unpoly构建完全相同的东西。Unpoly偏好一种略有不同的哲学:而不是让后端返回HTML片段,它更倾向于让服务器在每次XHR请求时返回完整的HTML页面,然后“拔出”相关元素(们)并将它们插入DOM中,替换旧的元素。
当涉及到表单时,Unpoly使用一个特殊属性[up-validate]
来标记当字段更改时应该触发表单提交和重新验证的字段。[up-validate]
的文档也将它描述为“一种在另一个字段的值依赖于另一个字段值时部分更新表单的好方法”,这就是我们将用来实现我们的级联选择。
表单与上面HTMX示例中的完全相同。但这次,我们只需要一个视图!
def unpoly_form(request):
form = MakeAndModelForm(request.POST or None)
return render(request, "unpoly.html", {"form": form})
urlpatterns = [
path("unpoly-form/", unpoly_form),
]
并且模板更加简单
{% load widget_tweaks %}
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/unpoly@2.5.0/unpoly.min.js"></script>
</head>
<body>
<form method="POST">
<h3>Pick a make/model</h3>
{% csrf_token %}
<div>
{{ form.make.label_tag }}
{% render_field form.make up-validate="form" %}
</div>
<div>
{{ form.model.label_tag }}
{{ form.model }}
</div>
</form>
</body>
</html>
include
参数
我们可能还需要一个功能:如果我们想从表单中完全删除一个字段,除非另一个字段有特定的值怎么办?为了实现这一点,DynamicField
构造函数接受一个特殊的参数,这个参数不会传递给包装字段的构造函数:include
。就像任何其他参数一样,这可以是一个可调用的参数,它接收表单实例,并应该返回一个布尔值:True
表示字段应该包含在表单中,False
表示不包含。以下是一个示例
class CancellationReasonForm(DynamicFormMixin, forms.Form):
CANCELLATION_REASONS = [
("too-expensive", "Too expensive"),
("too-boring", "Too boring"),
("other", "Other"),
]
cancellation_reason = forms.ChoiceField(choices=CANCELLATION_REASONS)
reason_if_other = DynamicField(
forms.CharField,
include=lambda form: form["cancellation_reason"].value() == "other",
)
已知的陷阱:可调用参数
可能会让你感到意外的一点:如果你传递给表单字段构造函数的对象已经是一个可调用的,你需要将其包装在一个另一个可调用中,该可调用接受 form
参数并返回你想要传递给字段的实际可调用。
这种情况最可能发生在你传递自定义小部件类时,因为类是可调用的
class CancellationReasonForm(DynamicFormMixin, forms.Form):
... # other fields
reason_if_other = DynamicField(
forms.CharField,
include=lambda form: form["cancellation_reason"].value() == "other",
widget=lambda _: forms.TextArea,
)
为什么名字这么尴尬?
因为 django-dynamic-forms
已经被占用了。
行为准则
有关贡献此存储库时的行为准则指南,请查阅 https://www.dabapps.com/open-source/code-of-conduct/
项目详情
下载文件
下载适合您平台的应用程序。如果您不确定选择哪个,请了解有关 安装软件包 的更多信息。
源代码分布
构建分布
django-forms-dynamic-1.0.0.tar.gz 的散列值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 38339fa12722c1eeebb2f22d9a996c80be19339f17de718784c16e2ba3d3bc6f |
|
MD5 | 1c92f8617d62b8ce11732704308ddb96 |
|
BLAKE2b-256 | 4d2d996798f02d20d3ea9ae0614358bbb75c67f50eaae662131f7d59052f2e8c |
django_forms_dynamic-1.0.0-py3-none-any.whl 的散列值
算法 | 散列摘要 | |
---|---|---|
SHA256 | 59ce7053b120727001cb1f21436d507017cbd8a91a0c65ebc731c399cbadab0c |
|
MD5 | afa600e7cc5ae1848461b803f50f390d |
|
BLAKE2b-256 | 541e0d4dec5b3a046788d08b7187b814db27d0697d340eb4d93ed78c81b48c58 |