Skip to content

Commit 03b62cc

Browse files
authored
Add demo of using context modifiers to render forms and form fields (#146)
1 parent 509e16d commit 03b62cc

File tree

10 files changed

+257
-7
lines changed

10 files changed

+257
-7
lines changed

docs/guides/defining-template-context.md

+3-4
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ context:
5858
5959
You can define a list or a dict or anything that [`PyYAML`](http://pyyaml.org/wiki/PyYAMLDocumentation) allows you to create in YAML format without creating a custom objects.
6060

61-
6261
## Modifying template contexts with Python
6362

6463
While most objects can be faked with YAML, Django has a few common constructs that are difficult to replicate. For example: `Form` and `Paginator` instances. To help with this, django-pattern-library allows you to register any number of 'context modifiers'. Context modifiers are simply Python functions that accept the `context` dictionary generated from the YAML file, and can make additions or updates to it as necessary. For convenience, they also receive the current `HttpRequest` as `request`.
@@ -135,7 +134,7 @@ def replicate_pagination(context, request):
135134

136135
### Registering a context modifier for a specific template
137136

138-
By default, context modifiers are applied to all pattern library templates. If you only wish for a context modifier to be applied to a specific pattern, you can use the ``template`` parameter to indicate this. For example:
137+
By default, context modifiers are applied to all pattern library templates. If you only wish for a context modifier to be applied to a specific pattern, you can use the `template` parameter to indicate this. For example:
139138

140139
```python
141140

@@ -166,9 +165,9 @@ def add_invalid_subscribe_form(context, request):
166165

167166
### Controlling the order in which context modifiers are applied
168167

169-
By default, context modifiers are applied in the order they were registered (which can be difficult to predict accross multiple apps), with generic context modifiers being applied first, followed by template-specific ones. If you need to control the order in which a series of context modifiers are applied, you can use the `order` parameter to do this.
168+
By default, context modifiers are applied in the order they were registered (which can be difficult to predict across multiple apps), with generic context modifiers being applied first, followed by template-specific ones. If you need to control the order in which a series of context modifiers are applied, you can use the `order` parameter to do this.
170169

171-
In the following example, a generic context modifier is registered with an `order` value of `1`, while others recieve the default value of `0`. Because `1` is higher than `0`, the generic context modifier will be applied **after** the others.
170+
In the following example, a generic context modifier is registered with an `order` value of `1`, while others receive the default value of `0`. Because `1` is higher than `0`, the generic context modifier will be applied **after** the others.
172171

173172
```python
174173

docs/recipes/forms-and-fields.md

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Forms and fields
2+
3+
Basic Django form definition:
4+
5+
```python
6+
from django import forms
7+
8+
9+
class ExampleForm(forms.Form):
10+
single_line_text = forms.CharField(
11+
max_length=255, help_text="This is some help text"
12+
)
13+
choices = (("one", "One"), ("two", "Two"), ("three", "Three"), ("four", "Four"))
14+
select = forms.ChoiceField(choices=choices)
15+
```
16+
17+
Rendered as:
18+
19+
```jinja2
20+
{% extends "patterns/base.html" %}
21+
22+
{% block content %}
23+
<form method="post" class="form">
24+
{% csrf_token %}
25+
<div class="form__container">
26+
{% for hidden_field in form.hidden_fields %}
27+
{{ hidden_field }}
28+
{% endfor %}
29+
30+
{% for field in form.visible_fields %}
31+
{% include "patterns/molecules/field/field.html" with field=field %}
32+
{% endfor %}
33+
34+
<button class="form__submit button" type="submit">Submit</button>
35+
</div>
36+
</form>
37+
{% endblock %}
38+
```
39+
40+
Context overrides when rendering the whole form:
41+
42+
```python
43+
from pattern_library import register_context_modifier
44+
from .forms import ExampleForm
45+
46+
47+
@register_context_modifier
48+
def add_common_forms(context, request):
49+
context['form'] = ExampleForm()
50+
```
51+
52+
Context overrides for `field.html`:
53+
54+
```python
55+
from pattern_library import register_context_modifier
56+
from .forms import ExampleForm
57+
58+
59+
@register_context_modifier(template='patterns/molecules/field/field.html')
60+
def add_field(context, request):
61+
form = ExampleForm()
62+
context['field'] = form['single_line_text']
63+
```

docs/reference/api.md

+19-3
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ context:
2727
tags:
2828
error_tag:
2929
include:
30-
template_name: 'non-patterns/include.html'
30+
template_name: "non-patterns/include.html"
3131
```
3232
3333
## Templates
@@ -121,20 +121,36 @@ PATTERN_LIBRARY = {
121121

122122
### `override_tag`
123123

124-
This function tells the pattern library which Django tags to override, and optionally supports providing a default value. See [../guides/overriding-template-tags.md] for more information.
124+
This function tells the pattern library which Django tags to override, and optionally supports providing a default value. See [Overriding template tags](../guides/overriding-template-tags.md) for more information.
125125

126126
```python
127127
from pattern_library.monkey_utils import override_tag
128128
129129
override_tag(register, 'a_tag_name', default_html="https://example.com/")
130130
```
131131

132+
## `register_context_modifier`
133+
134+
This decorator makes it possible to override or create additional context data with Django / Python code, rather than being limited to YAML. It has to be called from within a `pattern_contexts` module, which can be at the root of any Django app. See [Modifying template contexts with Python](../guides/defining-template-context.md#modifying-template-contexts-with-python) for more information.
135+
136+
```python
137+
# myproject/core/pattern_contexts.py
138+
139+
from pattern_library import register_context_modifier
140+
from myproject.core.forms import SearchForm, SignupForm
141+
142+
@register_context_modifier
143+
def add_common_forms(context, request):
144+
if 'search_form' not in context:
145+
context["search_form"] = SearchForm()
146+
```
147+
132148
## Commands
133149

134150
### `render_patterns`
135151

136152
Renders all django-pattern-library patterns to HTML files, in a directory
137-
structure. This can be useful for [automated tests](../guides/automated-tests.md)
153+
structure. This can be useful for [automated tests](../guides/automated-tests.md).
138154

139155
Usage:
140156

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ nav:
4343
- 'Recipes':
4444
- 'Inclusion tags': 'recipes/inclusion-tags.md'
4545
- 'Looping for tags': 'recipes/looping-for-tags.md'
46+
- 'Forms and fields': 'recipes/forms-and-fields.md'
4647
- 'Image include': 'recipes/image-include.md'
4748
- 'Image lazyload': 'recipes/image-lazyload.md'
4849
- 'Reference':

tests/forms.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django import forms
2+
3+
4+
class ExampleForm(forms.Form):
5+
single_line_text = forms.CharField(
6+
max_length=255, help_text="This is some help text"
7+
)
8+
choices = (("one", "One"), ("two", "Two"), ("three", "Three"), ("four", "Four"))
9+
select = forms.ChoiceField(choices=choices)

tests/pattern_contexts.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from pattern_library import register_context_modifier
2+
3+
from .forms import ExampleForm
4+
5+
6+
@register_context_modifier
7+
def add_common_forms(context, request):
8+
context['form'] = ExampleForm()
9+
10+
11+
@register_context_modifier(template='patterns/molecules/field/field.html')
12+
def add_field(context, request):
13+
form = ExampleForm()
14+
context['field'] = form['single_line_text']

tests/static/main.css

+58
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
html {
2+
box-sizing: border-box;
3+
}
4+
5+
*,
6+
*::before,
7+
*::after {
8+
box-sizing: inherit;
9+
}
10+
111
.icon {
212
fill: currentColor;
313
}
@@ -61,3 +71,51 @@
6171
max-width: 768px;
6272
padding-bottom: 5px;
6373
}
74+
75+
.field {
76+
margin-bottom: 20px;
77+
}
78+
79+
.field--errors {
80+
padding: 20px;
81+
margin-bottom: 20px;
82+
border: 1px dotted #f00;
83+
}
84+
85+
.field__required {
86+
color: #f00;
87+
}
88+
.field__label,
89+
.field__label--multiple {
90+
display: block;
91+
margin-bottom: 5px;
92+
font-weight: 700;
93+
}
94+
.field__errors {
95+
margin-bottom: 10px;
96+
font-weight: 700;
97+
color: #f00;
98+
}
99+
.field__help {
100+
margin-top: 5px;
101+
}
102+
.field input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]),
103+
.field textarea,
104+
.field select {
105+
width: 100%;
106+
padding: 10px;
107+
border: 1px solid #141414;
108+
}
109+
110+
.field select:not([multiple]) {
111+
background-color: #fff;
112+
padding: 0.5em 3.5em 0.5em 1em;
113+
appearance: none;
114+
background-image: linear-gradient(45deg, transparent 50%, #444 50%),
115+
linear-gradient(135deg, #444 50%, transparent 50%),
116+
linear-gradient(to right, rgba(20, 20, 20, 0.2), rgba(20, 20, 20, 0.2));
117+
background-position: calc(100% - 20px) calc(1em + 2px),
118+
calc(100% - 15px) calc(1em + 2px), calc(100% - 2.5em) 0.5em;
119+
background-size: 5px 5px, 5px 5px, 1px 1.5em;
120+
background-repeat: no-repeat;
121+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{% load test_tags %}
2+
{% with widget_type=field|widget_type %}
3+
<div class="field {% if widget_type %}field--{{ widget_type }}{% endif %} {% if field.errors %}field--errors{% endif %}">
4+
5+
{% if field.errors %}
6+
<div class="field__errors">
7+
{{ field.errors }}
8+
</div>
9+
{% endif %}
10+
11+
{% if widget_type == 'checkbox-select-multiple' or widget_type == 'radio-select' %}
12+
<fieldset class="field__fieldset">
13+
<legend class="field__label field__label--multiple">
14+
{{ field.label }}
15+
{% if field.field.required %}<span class="field__required" aria-hidden="true">*</span>{% endif %}
16+
</legend>
17+
18+
<ul>
19+
{% for boundwidget in field %}
20+
<li class="field__radio-checkbox">
21+
<input id="{{ boundwidget.id_for_label }}" type="{{ boundwidget.data.type }}" name="{{ boundwidget.data.name }}" value="{{ boundwidget.data.value }}"{% if boundwidget.data.selected %} checked{% endif %}>
22+
<label for="{{ boundwidget.id_for_label }}" class="field__label">{{ boundwidget.data.label }}</label>
23+
</li>
24+
{% endfor %}
25+
</ul>
26+
</fieldset>
27+
28+
{% elif widget_type == 'checkbox-input' %}
29+
30+
<div class="field__radio-checkbox">
31+
{{ field }}
32+
<label for="{{ field.id_for_label }}" class="field__label">
33+
{{ field.label }}
34+
{% if field.field.required %}<span class="field__required" aria-hidden="true">*</span>{% endif %}
35+
</label>
36+
</div>
37+
38+
{% else %}
39+
40+
<label for="{{ field.id_for_label }}" class="field__label">
41+
{{ field.label }}
42+
{% if field.field.required %}<span class="field__required" aria-hidden="true">*</span>{% endif %}
43+
</label>
44+
{{ field }}
45+
46+
{% endif %}
47+
48+
{% if field.help_text %}<div class="field__help">{{ field.help_text }}</div>{% endif %}
49+
50+
</div>
51+
{% endwith %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{% extends "patterns/base.html" %}
2+
3+
{% block content %}
4+
<form method="post" class="form">
5+
{% csrf_token %}
6+
<div class="form__container">
7+
{% if form.errors %}
8+
<div class="form__errors">
9+
There were some errors with your form. Please amend the fields highlighted below.
10+
11+
{% if form.non_field_errors %}
12+
<ul>
13+
{% for error in form.non_field_errors %}
14+
<li>{{ error }}</li>
15+
{% endfor %}
16+
</ul>
17+
{% endif %}
18+
</div>
19+
{% endif %}
20+
21+
{% for hidden_field in form.hidden_fields %}
22+
{{ hidden_field }}
23+
{% endfor %}
24+
25+
{% for field in form.visible_fields %}
26+
{% include "patterns/molecules/field/field.html" with field=field %}
27+
{% endfor %}
28+
29+
<button class="form__submit button" type="submit">Submit</button>
30+
</div>
31+
</form>
32+
{% endblock %}

tests/templatetags/test_tags.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django import template
2+
from django.utils.text import camel_case_to_spaces, slugify
23

34
from pattern_library.monkey_utils import override_tag
45

@@ -26,6 +27,12 @@ def default_html_tag_falsey(arg=None):
2627
raise Exception("default_tag raised an exception")
2728

2829

30+
# Get widget type of a field
31+
@register.filter(name="widget_type")
32+
def widget_type(bound_field):
33+
return slugify(camel_case_to_spaces(bound_field.field.widget.__class__.__name__))
34+
35+
2936
override_tag(register, 'error_tag')
3037
override_tag(register, 'default_html_tag', default_html="https://potato.com")
3138
override_tag(register, 'default_html_tag_falsey', default_html=None)

0 commit comments

Comments
 (0)