Skip to content

Commit 8a5559b

Browse files
Merge branch 'main' into css-transitions
2 parents 54a0676 + b6cf14d commit 8a5559b

File tree

20 files changed

+906
-153
lines changed

20 files changed

+906
-153
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
tags:
66
- "*.*.*"
7+
workflow_dispatch:
78

89
permissions:
910
contents: read

DEVELOPING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
1. Update `docs/source/changelog.md`
4040
1. Update version in `pyproject.toml`
4141
1. Commit changes
42-
1. Tag the release: `git tag 0.65.0`
42+
1. Tag the release: `git tag #.##.#`
4343
1. Push the tag: `git push origin --tags`
4444
1. The GitHub Action will automatically:
4545
- Build the JavaScript assets with the correct version

docs/source/changelog.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
# Changelog
22

3+
## 0.66.0
4+
5+
**Features**
6+
- Implement `unicorn:dirty` targeting support.
7+
- Add signals for lifecycle hooks.
8+
- Implement script tag loading from newly added child components.
9+
- Implement popstate support for history navigation.
10+
- Implement a timestamp-based (epoch) tracking mechanism to identify and discard stale responses on the frontend.
11+
12+
**Fixes**
13+
- Surface broken import errors when loading components.
14+
- Handle nested method calls in `parse_call_method_name`.
15+
- Restore float values in nested `tuple[dict]` fields.
16+
- Fix: `u:loading` on child elements no longer disables parent `u:click`.
17+
- Fix: Trigger loading states for actions called via `Unicorn.call()`.
18+
- Fix: Include child component JavaScript calls in response.
19+
- Fix `ValueError` when deserializing `ForeignKey` fields on models loaded outside `mount`.
20+
21+
**Refactors**
22+
- Consolidate component metadata (checksum, hash, epoch) into 'meta' identifier.
23+
- Remove redundant per-component script tags and enable global DOM scanning.
24+
25+
**Documentation**
26+
- Add tutorial documentation covering inputs, actions, and polling.
27+
- Add guide on table structure limitations.
28+
- Clarify Unicorn can be adopted incrementally.
29+
- Add Context Processors and Component Re-rendering section.
30+
- Improve Installation Tutorial and Add Pagination Documentation.
31+
332
## 0.65.0
433

534
- Properly escape single quotes in Unicorn.call() arguments [#773](https://github.com/adamghill/django-unicorn/pull/773) by [JohananOppongAmoateng](https://github.com/JohananOppongAmoateng).

docs/source/templates.md

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,113 @@ class HelloWorldView(UnicornView):
6565
[Django models](django-models.md) has many more details about using Django models in `Unicorn`.
6666
```
6767

68+
## Models inside `{% for %}` loops
69+
70+
When iterating over a list with a Django `{% for %}` loop, the loop variable is **not** the same as the component attribute that holds the list. Unicorn syncs `<input>` values by looking up the `unicorn:model` name in the component's serialised data, so the model name must use the **component attribute path** (e.g. `items.0.name`), not the loop variable name (e.g. `item.name`).
71+
72+
Use `{{ forloop.counter0 }}` to build the correct index-based path:
73+
74+
```python
75+
# line_items.py
76+
from django_unicorn.components import UnicornView
77+
78+
class LineItemsView(UnicornView):
79+
items: list = []
80+
81+
def mount(self):
82+
self.items = [{"name": "Widget", "qty": 1}, {"name": "Gadget", "qty": 3}]
83+
84+
def add_item(self):
85+
self.items.append({"name": "", "qty": 0})
86+
```
87+
88+
```html
89+
<!-- unicorn/line-items.html -->
90+
<div>
91+
{% for item in items %}
92+
<div>
93+
<input
94+
unicorn:model="items.{{ forloop.counter0 }}.name"
95+
type="text"
96+
/>
97+
<input
98+
unicorn:model="items.{{ forloop.counter0 }}.qty"
99+
type="number"
100+
/>
101+
</div>
102+
{% endfor %}
103+
<button unicorn:click="add_item">Add row</button>
104+
</div>
105+
```
106+
107+
The key is `items.{{ forloop.counter0 }}.name` this renders to `items.0.name`, `items.1.name`, etc., which Unicorn can resolve directly against the component data.
108+
109+
```{warning}
110+
Using the **loop variable name** in `unicorn:model` (e.g. `unicorn:model="item.name"`)
111+
will **not** work correctly after a re-render. When the component re-renders,
112+
Unicorn tries to look up `item` in the component's data, but `item` is only a
113+
Django template loop variable — it has no corresponding key in the serialised
114+
component state. As a result morphdom will clear the input's value.
115+
116+
Use `items.{{ forloop.counter0 }}.name` instead.
117+
```
118+
119+
## Rendering choice fields
120+
121+
Django models often use `TextChoices` (or `IntegerChoices`) to constrain a field to a fixed set of values. To render a reactive `<select>` that is bound to a Unicorn component field, expose the choices list as a component attribute and exclude it from the JavaScript context (since the choices are static and do not need to be reactive).
122+
123+
```python
124+
# new_course.py
125+
from django_unicorn.components import UnicornView
126+
from curriculum.models import Course
127+
128+
129+
class NewCourseView(UnicornView):
130+
grade_level = Course.GradeLevel.UNDEFINED
131+
subject = Course.Subject.UNDEFINED
132+
133+
# Static choices — kept out of the JSON state sent to the browser
134+
grade_level_choices = Course.GradeLevel.choices
135+
subject_choices = Course.Subject.choices
136+
137+
class Meta:
138+
javascript_exclude = ("grade_level_choices", "subject_choices")
139+
140+
def save(self):
141+
Course.objects.create(
142+
grade_level=self.grade_level,
143+
subject=self.subject,
144+
)
145+
```
146+
147+
```html
148+
<!-- unicorn/new-course.html -->
149+
<div>
150+
<select unicorn:model="grade_level">
151+
{% for value, label in grade_level_choices %}
152+
<option value="{{ value }}">{{ label }}</option>
153+
{% endfor %}
154+
</select>
155+
156+
<select unicorn:model="subject">
157+
{% for value, label in subject_choices %}
158+
<option value="{{ value }}">{{ label }}</option>
159+
{% endfor %}
160+
</select>
161+
162+
<button unicorn:click="save">Save</button>
163+
</div>
164+
```
165+
166+
Adding the choices to [`Meta.javascript_exclude`](views.md#javascript_exclude) keeps them in the Django template context (so the `{% for %}` loop works) without serialising them into the `unicorn:data` JSON attribute on every render. The `grade_level` and `subject` fields remain fully reactive — Unicorn syncs the selected value back to the component on each change.
167+
168+
```{note}
169+
If you also need validation, set `form_class` on the component to a Django `ModelForm` or `Form`.
170+
Unicorn will validate `grade_level` and `subject` against the form's field definitions when
171+
`$validate` is called or when an action method calls `self.validate()`. See
172+
[validation](validation.md) for details.
173+
```
174+
68175
## Model modifiers
69176

70177
### Lazy

docs/source/views.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,45 @@ class HelloKwargView(UnicornView):
195195
assert self.component_kwargs["hello"] == "World"
196196
```
197197

198+
### Passing a Django Form
199+
200+
A Django `Form` (or `ModelForm`) instance can be passed directly from a template into a unicorn component as a keyword argument. The form will be available in the component's template context for rendering, but it is automatically excluded from the JSON state sent to the browser (since forms cannot be serialized to JSON).
201+
202+
```html
203+
<!-- index.html -->
204+
{% unicorn 'my-form-component' form=my_django_form %}
205+
```
206+
207+
```python
208+
# my_form_component.py
209+
from django_unicorn.components import UnicornView
210+
211+
class MyFormComponentView(UnicornView):
212+
form = None # will hold the passed-in form instance
213+
214+
def mount(self):
215+
# self.form is available here on the initial render
216+
pass
217+
```
218+
219+
```html
220+
<!-- unicorn/my-form-component.html -->
221+
<div>
222+
<form method="POST">
223+
{% csrf_token %}
224+
{{ form.as_p }}
225+
<button unicorn:click="submit">Submit</button>
226+
</form>
227+
</div>
228+
```
229+
230+
```{note}
231+
Because forms cannot be pickled, `self.form` will be `None` on subsequent AJAX
232+
interactions (after the initial page load). If you need to process submitted form
233+
data reactively, declare a `form_class` on the component and use
234+
[component validation](validation.md) instead.
235+
```
236+
198237
### request
199238

200239
The current `request`.
@@ -392,7 +431,12 @@ class HelloStateView(UnicornView):
392431

393432
### javascript_exclude
394433

395-
To allow an attribute to be included in the the context to be used by a Django template, but not exposed to JavaScript, add it to the `Meta` class's `javascript_exclude` tuple.
434+
To allow an attribute to be included in the context to be used by a Django template, but not exposed to JavaScript, add it to the `Meta` class's `javascript_exclude` tuple.
435+
436+
```{note}
437+
Django `Form` and `ModelForm` instances are **automatically** excluded from the
438+
JavaScript context — you do not need to add them to `javascript_exclude`.
439+
```
396440

397441
```html
398442
<!-- hello-state.html -->
@@ -465,4 +509,12 @@ Do not store unpickleable objects (e.g. generators) on the component instance.
465509

466510
If you need to use an unpickleable object, either convert it to a pickleable type (e.g. convert a generator to a list) or re-initialize it within the method that needs it without storing it on `self`.
467511

512+
```{note}
513+
Django `Form` and `ModelForm` instances are handled automatically — they are stripped
514+
from the component before pickling and restored afterwards, so passing a form as a
515+
template kwarg (see [Passing a Django Form](#passing-a-django-form)) will not cause
516+
pickling errors. The form will be `None` after a cache restore (i.e. on subsequent
517+
AJAX requests).
518+
```
519+
468520

0 commit comments

Comments
 (0)