Skip to content

Commit 015f9bf

Browse files
authored
Merge branch 'main' into checksum-errors
2 parents 4b222f5 + b6cf14d commit 015f9bf

File tree

20 files changed

+904
-151
lines changed

20 files changed

+904
-151
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
@@ -205,6 +205,45 @@ class HelloKwargView(UnicornView):
205205
assert self.component_kwargs["hello"] == "World"
206206
```
207207

208+
### Passing a Django Form
209+
210+
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).
211+
212+
```html
213+
<!-- index.html -->
214+
{% unicorn 'my-form-component' form=my_django_form %}
215+
```
216+
217+
```python
218+
# my_form_component.py
219+
from django_unicorn.components import UnicornView
220+
221+
class MyFormComponentView(UnicornView):
222+
form = None # will hold the passed-in form instance
223+
224+
def mount(self):
225+
# self.form is available here on the initial render
226+
pass
227+
```
228+
229+
```html
230+
<!-- unicorn/my-form-component.html -->
231+
<div>
232+
<form method="POST">
233+
{% csrf_token %}
234+
{{ form.as_p }}
235+
<button unicorn:click="submit">Submit</button>
236+
</form>
237+
</div>
238+
```
239+
240+
```{note}
241+
Because forms cannot be pickled, `self.form` will be `None` on subsequent AJAX
242+
interactions (after the initial page load). If you need to process submitted form
243+
data reactively, declare a `form_class` on the component and use
244+
[component validation](validation.md) instead.
245+
```
246+
208247
### request
209248

210249
The current `request`.
@@ -402,7 +441,12 @@ class HelloStateView(UnicornView):
402441

403442
### javascript_exclude
404443

405-
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.
444+
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.
445+
446+
```{note}
447+
Django `Form` and `ModelForm` instances are **automatically** excluded from the
448+
JavaScript context — you do not need to add them to `javascript_exclude`.
449+
```
406450

407451
```html
408452
<!-- hello-state.html -->
@@ -582,4 +626,12 @@ Do not store unpickleable objects (e.g. generators) on the component instance.
582626

583627
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`.
584628

629+
```{note}
630+
Django `Form` and `ModelForm` instances are handled automatically — they are stripped
631+
from the component before pickling and restored afterwards, so passing a form as a
632+
template kwarg (see [Passing a Django Form](#passing-a-django-form)) will not cause
633+
pickling errors. The form will be `None` after a cache restore (i.e. on subsequent
634+
AJAX requests).
635+
```
636+
585637

0 commit comments

Comments
 (0)