django-nestingdolls 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. django_nestingdolls-1.0.0/AGENTS.md +49 -0
  2. django_nestingdolls-1.0.0/CHANGELOG.md +5 -0
  3. django_nestingdolls-1.0.0/LICENSE +29 -0
  4. django_nestingdolls-1.0.0/MANIFEST.in +8 -0
  5. django_nestingdolls-1.0.0/PKG-INFO +497 -0
  6. django_nestingdolls-1.0.0/README.md +466 -0
  7. django_nestingdolls-1.0.0/django_nestingdolls.egg-info/PKG-INFO +497 -0
  8. django_nestingdolls-1.0.0/django_nestingdolls.egg-info/SOURCES.txt +44 -0
  9. django_nestingdolls-1.0.0/django_nestingdolls.egg-info/dependency_links.txt +1 -0
  10. django_nestingdolls-1.0.0/django_nestingdolls.egg-info/requires.txt +1 -0
  11. django_nestingdolls-1.0.0/django_nestingdolls.egg-info/top_level.txt +1 -0
  12. django_nestingdolls-1.0.0/nestingdolls/AGENTS.md +154 -0
  13. django_nestingdolls-1.0.0/nestingdolls/README.md +416 -0
  14. django_nestingdolls-1.0.0/nestingdolls/__init__.py +54 -0
  15. django_nestingdolls-1.0.0/nestingdolls/apps.py +17 -0
  16. django_nestingdolls-1.0.0/nestingdolls/boundfield.py +515 -0
  17. django_nestingdolls-1.0.0/nestingdolls/errors.py +91 -0
  18. django_nestingdolls-1.0.0/nestingdolls/fields.py +873 -0
  19. django_nestingdolls-1.0.0/nestingdolls/patches.py +123 -0
  20. django_nestingdolls-1.0.0/nestingdolls/py.typed +0 -0
  21. django_nestingdolls-1.0.0/nestingdolls/static/AGENTS.md +184 -0
  22. django_nestingdolls-1.0.0/nestingdolls/static/nestingdolls/sequence.js +311 -0
  23. django_nestingdolls-1.0.0/nestingdolls/static/nestingdolls/sequence.ts +466 -0
  24. django_nestingdolls-1.0.0/nestingdolls/templates/AGENTS.md +52 -0
  25. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/mapping/attrs.html +4 -0
  26. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/mapping/div.html +15 -0
  27. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/mapping/p.html +26 -0
  28. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/mapping/table.html +19 -0
  29. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/mapping/ul.html +17 -0
  30. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/actions.html +33 -0
  31. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/deleted_rows.html +10 -0
  32. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/div.html +34 -0
  33. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/help_text.html +4 -0
  34. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/hidden.html +6 -0
  35. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/p.html +38 -0
  36. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/row.html +22 -0
  37. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/row_content.html +26 -0
  38. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/table.html +36 -0
  39. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/sequence/ul.html +34 -0
  40. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/shared/errors.html +4 -0
  41. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/shared/field_label.html +7 -0
  42. django_nestingdolls-1.0.0/nestingdolls/templates/nestingdolls/shared/help_text.html +8 -0
  43. django_nestingdolls-1.0.0/nestingdolls/widgets.py +975 -0
  44. django_nestingdolls-1.0.0/pyproject.toml +120 -0
  45. django_nestingdolls-1.0.0/setup.cfg +4 -0
  46. django_nestingdolls-1.0.0/tests/AGENTS.md +145 -0
@@ -0,0 +1,49 @@
1
+ ## Scope and guides
2
+
3
+ This repository-wide guide applies everywhere; read it before the guide for the
4
+ directory you edit. A nested guide supplements, rather than replaces, this one.
5
+
6
+ - Before package Python edits, read
7
+ [`nestingdolls/AGENTS.md`](nestingdolls/AGENTS.md); read its **Input limits**
8
+ section before changing normalization, extraction, or row counts.
9
+ - Before creating or changing a test, read
10
+ [`tests/AGENTS.md`](tests/AGENTS.md), the canonical test guide.
11
+ - Before editing `sequence.ts` or `tests/test_sequence.mjs`, read
12
+ [`nestingdolls/static/AGENTS.md`](nestingdolls/static/AGENTS.md).
13
+ - Before editing a Django template, read
14
+ [`nestingdolls/templates/AGENTS.md`](nestingdolls/templates/AGENTS.md).
15
+
16
+ ## Layout
17
+
18
+ - `nestingdolls/` contains fields, widgets, bound fields, errors, and the
19
+ form-layout render patch; its guide maps modules to concerns and tests.
20
+ `nestingdolls/static/nestingdolls/` contains `sequence.ts` and its committed
21
+ compiled artifact, `sequence.js`; `nestingdolls/templates/nestingdolls/`
22
+ contains widget templates.
23
+ - `tests/` contains Python tests (`test_*.py`) and the jsdom tests
24
+ (`test_sequence.mjs`). The root also contains `demo.py`, `pathological.py`
25
+ (hostile-submission cost measurements), and `mypy_settings.py`.
26
+
27
+ ## Commands and verification
28
+
29
+ All commands below are Make targets; use `make help` to list them. A fresh
30
+ clone needs `npm ci` before JavaScript targets (`tscheck`, `jsdrift`, `jstest`,
31
+ `js`) and therefore before `make check`.
32
+
33
+ - `make check` is the non-mutating CI gate: `tscheck`, `jsdrift`, `jstest`,
34
+ `ruff`, `formatcheck`, `test`, `distcheck`, and `mypy`.
35
+ - `make fix` applies lint auto-fixes and formatting; `make check` changes no
36
+ files.
37
+ - Run `make check` after implementation changes. Do not run runtime checks for
38
+ prose-only changes unless executable examples changed.
39
+ - `make mypy` checks `demo.py` and `nestingdolls` using `mypy_settings.py` for
40
+ `django-stubs`; tests do not import that file. `make ruff` and
41
+ `make formatcheck` cover the maintained Python files in the Makefile's
42
+ `PYTHON_FILES`.
43
+ - `make distcheck` builds both distributions from the tracked tree in a
44
+ temporary directory, runs `twine check --strict`, and verifies that the
45
+ sdist includes `LICENSE`; it uses `git write-tree`, so untracked build inputs
46
+ fail the gate.
47
+ - After editing `sequence.ts`, follow the static guide's build order.
48
+ `make check` compares `sequence.js` with the current TypeScript build using
49
+ `cmp`, not Git.
@@ -0,0 +1,5 @@
1
+ # Change history
2
+
3
+ ## 1.0.0
4
+
5
+ - Initial release. 🤷‍♀️
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Keryn Knight
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,8 @@
1
+ prune tests
2
+
3
+ include AGENTS.md
4
+ include nestingdolls/AGENTS.md
5
+ include CHANGELOG.md
6
+ include nestingdolls/static/AGENTS.md
7
+ include nestingdolls/templates/AGENTS.md
8
+ include tests/AGENTS.md
@@ -0,0 +1,497 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-nestingdolls
3
+ Version: 1.0.0
4
+ Summary: Composite Django form fields for nested mappings and sequences.
5
+ Author-email: Keryn Knight <keryn@kerynknight.com>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/kezabelle/django-nestingdolls
8
+ Project-URL: Source, https://github.com/kezabelle/django-nestingdolls
9
+ Project-URL: Issues, https://github.com/kezabelle/django-nestingdolls/issues
10
+ Project-URL: Changelog, https://github.com/kezabelle/django-nestingdolls/blob/main/CHANGELOG.md
11
+ Keywords: django,forms,formsets,fields,nested,composite
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 5.2
16
+ Classifier: Framework :: Django :: 6.0
17
+ Classifier: Framework :: Django :: 6.1
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.12
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: django>=5.2
30
+ Dynamic: license-file
31
+
32
+ ![django-nestingdolls](https://raw.githubusercontent.com/kezabelle/django-nestingdolls/main/nestingdolls.png)
33
+
34
+ # nestingdolls
35
+
36
+ Django's ordinary form fields are good for flat data, but fall _**flat**_ (hah!)
37
+ as soon as you might want any nesting, forcing instead toward separate formset
38
+ configuration and orchestration. It'd be nicer if it were simpler, and more
39
+ declarative...
40
+
41
+ You know that thing where a form has collected an address, an order, or a
42
+ schedule, then hands you a flat dictionary and says, good luck with that?
43
+ It works, but the awkward part is that your application has to put the related
44
+ keys back together.
45
+
46
+ `django-nestingdolls` does that bit for you. `DictField` renders and cleans a
47
+ child Django `Form`, then returns its cleaned values as one `dict`. `ListField`
48
+ renders formset-shaped rows for one child Django `Field`, then returns their
49
+ cleaned values as one `list`. The outer form gets the value it was describing
50
+ in the first place.
51
+
52
+ The child forms and fields still do the grown-up work: widgets, type
53
+ conversion, validation, file uploads, and errors. These fields just make sure
54
+ the bits that belong together don't wander off and become loose keys.
55
+
56
+ ## A micro example
57
+
58
+ Here's a small snapshot of how simple it is, with a form which allows for
59
+ multiple subforms:
60
+
61
+ ```python
62
+
63
+ # normal Django form!
64
+ class LineItemForm(forms.Form):
65
+ description = forms.CharField()
66
+ quantity = forms.IntegerField(min_value=1)
67
+
68
+
69
+ class ListOfMappingsForm(forms.Form):
70
+ # normal Django field
71
+ delivery_method = forms.ChoiceField(choices=[("standard", "Standard"), ("express", "Express")])
72
+ # normal Django field
73
+ contact_email = forms.EmailField()
74
+ # here's our special magic new fields!
75
+ items = nestingdolls.ListField(nestingdolls.DictField(LineItemForm), min_length=1, max_length=5)
76
+ ```
77
+
78
+ and that small definition gives us:
79
+
80
+ ![list of mappings](https://raw.githubusercontent.com/kezabelle/django-nestingdolls/main/nestingdolls-list-of-dicts.gif)
81
+
82
+ (this example is taken from the git repository's `demo.py`)
83
+
84
+ ## Install
85
+
86
+ `django-nestingdolls` expects Python 3.12+ and Django 5.2+.
87
+
88
+ ```sh
89
+ python -m pip install django-nestingdolls
90
+ ```
91
+
92
+ or add it to your `pyproject` and `uv sync` or whatever, poetry? pdm? you
93
+ do you.
94
+
95
+ ### Setup
96
+
97
+ Then add the app to your Django settings module:
98
+
99
+ ```python
100
+ INSTALLED_APPS = [
101
+ # ...
102
+ 'nestingdolls',
103
+ ]
104
+ ```
105
+
106
+ That's the whole thing, hopefully. Django's normal DTL form renderer finds
107
+ the package templates and the matching composite wrappers for the `as_div()`,
108
+ `as_p()`, `as_table()`, and `as_ul()` form helpers.
109
+
110
+ The app setup also makes composite wrappers follow Django's form-helper
111
+ layout. [Why that needs a patch](#the-render-patch).
112
+
113
+ ## A quick tutorial
114
+
115
+ Let's make a `CheckoutForm`. The browser will send ordinary Django input
116
+ names; the finished form will hand us one nested `order` dictionary with
117
+ integer product IDs and quantities. Nice and tidy.
118
+
119
+ ### Describe the order
120
+
121
+ Start small: a line item has two named values. An order has a customer ID and
122
+ as many of those items as it needs. Put the forms together like this:
123
+
124
+ ```python
125
+ from django import forms
126
+
127
+ import nestingdolls
128
+
129
+
130
+ class LineItemForm(forms.Form):
131
+ product_id = forms.IntegerField(min_value=1)
132
+ quantity = forms.IntegerField(min_value=1)
133
+
134
+
135
+ class OrderFieldsForm(forms.Form):
136
+ customer_id = forms.IntegerField(min_value=1) # Obviously not really :)
137
+ items = nestingdolls.ListField(
138
+ nestingdolls.DictField(LineItemForm),
139
+ min_length=1,
140
+ )
141
+
142
+
143
+ class CheckoutForm(forms.Form):
144
+ order = nestingdolls.DictField(OrderFieldsForm)
145
+ ```
146
+
147
+ `LineItemForm` checks products and quantities. `OrderFieldsForm` checks the
148
+ customer and item list. `CheckoutForm` gets one `order` value back, not a
149
+ scavenger hunt through keys that merely look like they are related.
150
+
151
+ ### Bind browser data
152
+
153
+ Browsers only send strings, and nested child fields get prefixed names.
154
+ Sequence rows bring Django's usual formset management values along too. Here is
155
+ what a complete two-item submission looks like:
156
+
157
+ ```python
158
+ from django.http import QueryDict
159
+
160
+ # This is the data the user submitted for their order form from the
161
+ # previous section above.
162
+ submission = QueryDict(
163
+ 'order-customer_id=17&'
164
+ 'order-items-TOTAL_FORMS=2&'
165
+ 'order-items-INITIAL_FORMS=0&'
166
+ 'order-items-MIN_NUM_FORMS=0&'
167
+ 'order-items-MAX_NUM_FORMS=1000&'
168
+ 'order-items-0-product_id=42&'
169
+ 'order-items-0-quantity=2&'
170
+ 'order-items-1-product_id=73&'
171
+ 'order-items-1-quantity=1'
172
+ )
173
+
174
+ form = CheckoutForm(submission)
175
+
176
+ assert form.is_valid()
177
+ assert form.cleaned_data == {
178
+ 'order': {
179
+ 'customer_id': 17, # example only :D
180
+ 'items': [
181
+ {'product_id': 42, 'quantity': 2},
182
+ {'product_id': 73, 'quantity': 1},
183
+ ],
184
+ }
185
+ }
186
+ ```
187
+
188
+ See the hand-off? `IntegerField` turns `order-items-0-quantity` from a request
189
+ string into an integer. `CheckoutForm` then returns one `order` value ready for
190
+ order code. No reconstruction helper, properly nested into conceptual
191
+ namespaces, less hunting around hopefully.
192
+
193
+ ### Render it
194
+
195
+ Render it with Django's normal form helpers:
196
+
197
+ ```django
198
+ <form method='post'>
199
+ {% csrf_token %}
200
+ {{ form.media }}
201
+ {{ form.as_div }}
202
+ <button type='submit'>Place order</button>
203
+ </form>
204
+ ```
205
+
206
+ `form.media` loads `nestingdolls/sequence.js`, so people can add and remove
207
+ item rows without a reload. Kinda like in the Django admin inlines. Django
208
+ still names the controls, builds the rows, and does all the validation on
209
+ submitted values.
210
+
211
+ ## Show a saved order again
212
+
213
+ Give `initial` the nested Python value you saved, not prefixed browser names:
214
+
215
+ ```python
216
+ saved_order = {
217
+ 'customer_id': 17,
218
+ 'items': [
219
+ {'product_id': 42, 'quantity': 2},
220
+ {'product_id': 73, 'quantity': 1},
221
+ ],
222
+ }
223
+
224
+ form = CheckoutForm(initial={'order': saved_order})
225
+ ```
226
+
227
+ `order-items-0-product_id` is an HTTP submission name. It only exists after
228
+ Django has rendered and named real controls. `initial` wants ordinary Python
229
+ data, from before that whole naming circus starts.
230
+
231
+ ## More ways to use the fields
232
+
233
+ The checkout example follows the full browser path. The same fields should also
234
+ provide value at other boundaries: an API, an import job, or a background
235
+ worker.
236
+
237
+ ### Bind decoded application data
238
+
239
+ Got data from an API client or worker? Hand over the saved order directly.
240
+ Unlike `initial`, this is a submission, so Django validates it and puts it in
241
+ `cleaned_data` with everything else.
242
+
243
+ ```python
244
+ form = CheckoutForm({'order': saved_order})
245
+
246
+ assert form.is_valid()
247
+ assert form.cleaned_data['order'] == saved_order
248
+ ```
249
+
250
+ ### Validate an imported JSON, YAML, or CSV list
251
+
252
+ Decode the import to normal Python data first. Then let the child field do its
253
+ thing: convert and validate every value.
254
+
255
+ ```python
256
+ import csv
257
+ import json
258
+
259
+ import yaml
260
+
261
+
262
+ class ProductImportForm(forms.Form):
263
+ product_ids = nestingdolls.ListField(forms.IntegerField(min_value=1))
264
+
265
+
266
+ json_form = ProductImportForm(json.loads('{"product_ids": [42, 73]}'))
267
+ yaml_form = ProductImportForm(
268
+ yaml.safe_load('product_ids:\n - 42\n - 73')
269
+ )
270
+ # The list could come from a csv.DictReader row etc. too.
271
+ csv_form = ProductImportForm({'product_ids': next(csv.reader(['42,73']))})
272
+
273
+ for form in (json_form, yaml_form, csv_form):
274
+ assert form.is_valid()
275
+ assert form.cleaned_data['product_ids'] == [42, 73]
276
+ ```
277
+
278
+ Use `yaml.safe_load`, not `yaml.load`.
279
+
280
+ ### Constrain a repeated field
281
+
282
+ Use `min_length` and `max_length` when a list needs a useful range, not just
283
+ any number of rows.
284
+
285
+ ```python
286
+ class TeamForm(forms.Form):
287
+ member_ids = nestingdolls.ListField(
288
+ forms.IntegerField(min_value=1),
289
+ min_length=1,
290
+ max_length=10,
291
+ )
292
+ ```
293
+
294
+ A submitted list of one through ten valid IDs cleans normally. More than ten
295
+ gets a validation error.
296
+
297
+ ### Return a frozenset or another domain value
298
+
299
+ You don't have to settle for a mutable `list` or `dict` for your cleaned
300
+ value.
301
+
302
+ Use `FrozenSetField` when repeated values should be unique and stay immutable.
303
+ `TupleField`, `DataclassField`, and `NamedTupleField` make the other return
304
+ shapes in this example; see the [field reference](#the-fields-included) for
305
+ their parameters.
306
+
307
+ ```python
308
+ from dataclasses import dataclass
309
+ from typing import NamedTuple
310
+
311
+
312
+ @dataclass
313
+ class Dimensions:
314
+ width: int
315
+ height: int
316
+
317
+
318
+ class Limits(NamedTuple):
319
+ low: int
320
+ high: int
321
+
322
+
323
+ class DimensionsForm(forms.Form):
324
+ width = forms.IntegerField(min_value=1)
325
+ height = forms.IntegerField(min_value=1)
326
+
327
+
328
+ class LimitsForm(forms.Form):
329
+ low = forms.IntegerField()
330
+ high = forms.IntegerField()
331
+
332
+
333
+ class PreferencesForm(forms.Form):
334
+ pinned_product_ids = nestingdolls.FrozenSetField(forms.IntegerField())
335
+ dimensions = nestingdolls.DataclassField(DimensionsForm, output=Dimensions)
336
+ limits = nestingdolls.NamedTupleField(LimitsForm, output=Limits)
337
+
338
+
339
+ form = PreferencesForm(
340
+ {
341
+ 'pinned_product_ids': [42, '73', 42],
342
+ 'dimensions': {'width': '10', 'height': 20},
343
+ 'limits': {'low': 1, 'high': '5'},
344
+ }
345
+ )
346
+
347
+ assert form.is_valid()
348
+ assert form.cleaned_data['pinned_product_ids'] == frozenset({42, 73})
349
+ assert form.cleaned_data['dimensions'] == Dimensions(width=10, height=20)
350
+ assert form.cleaned_data['limits'] == Limits(low=1, high=5)
351
+ ```
352
+
353
+ ## Keep uploads with their row
354
+
355
+ When a repeated row has ordinary values and an upload, make that row a child
356
+ form. Then bind it the normal multipart way:
357
+
358
+ ```python
359
+ class AttachmentForm(forms.Form):
360
+ description = forms.CharField()
361
+ document = forms.FileField()
362
+
363
+
364
+ class EvidenceForm(forms.Form):
365
+ attachments = nestingdolls.ListField(
366
+ nestingdolls.DictField(AttachmentForm),
367
+ min_length=1,
368
+ )
369
+
370
+
371
+ form = EvidenceForm(request.POST, request.FILES)
372
+ ```
373
+
374
+ Render it with `enctype='multipart/form-data'`. `FileField` handles the
375
+ upload; the list field keeps every file beside its description. They arrived
376
+ together, so they can leave together.
377
+
378
+ ## The fields included
379
+
380
+ As mentioned at the beginning, high level, the 2 basic building blocks are
381
+ [`DictField`](#dictfield) (for nested key/value shaped data) and
382
+ [`ListField`](#listfield) (for "up to N of this field" data) and everything
383
+ else is a variant of those.
384
+
385
+ ### `DictField`
386
+
387
+ Renders and cleans a child `Form`, returning its cleaned values as a `dict`
388
+ by default.
389
+
390
+ Should you prefer, you can also use `FormField` or `Subform` or `MappingField`
391
+ (they're all aliases for the same thing).
392
+
393
+ | Parameter | What it does |
394
+ | --- | --- |
395
+ | `form_class` | The child Django `Form` class that supplies the named fields. |
396
+ | `output` | A callable that turns the cleaned child values into the returned value; defaults to `dict`. |
397
+
398
+ ### `NamedTupleField`
399
+
400
+ A subclass of the `DictField` which returns a `NamedTuple` instance instead.
401
+
402
+ | Parameter | What it does |
403
+ | --- | --- |
404
+ | `form_class` | The child Django `Form` class that supplies the named fields. |
405
+ | `output` | The `NamedTuple` class used to build the returned value. |
406
+
407
+ ### `DataclassField`
408
+
409
+ Like the `NamedTupleField` above, it's a subclass of the `DictField` but
410
+ it returns a `dataclass` instance. Shocking, I'm sure.
411
+
412
+ | Parameter | What it does |
413
+ | --- | --- |
414
+ | `form_class` | The child Django `Form` class that supplies the named fields. |
415
+ | `output` | The dataclass used to build the returned value. |
416
+
417
+ ### `ListField`
418
+
419
+ Renders and cleans any number of one child field, returning the cleaned rows as
420
+ a list. Basically the approximate equivalent of a formset, but without the
421
+ ceremony of handling it separately.
422
+
423
+ Also available as `SequenceField`.
424
+
425
+ | Parameter | What it does |
426
+ | --- | --- |
427
+ | `child_field` | The Django field repeated for every row. |
428
+ | `min_length` | The fewest cleaned rows allowed. |
429
+ | `max_length` | The most cleaned rows allowed. |
430
+ | `absolute_max` | The hard cap on submitted rows, including rows Django must reject before cleaning. |
431
+
432
+ ### `TupleField` (`FrozenSequenceField`)
433
+
434
+ Your standard `ListField`, but returns an immutable tuple.
435
+
436
+ | Parameter | What it does |
437
+ | --- | --- |
438
+ | `child_field` | The Django field repeated for every row. |
439
+ | `min_length` | The fewest cleaned rows allowed. |
440
+ | `max_length` | The most cleaned rows allowed. |
441
+ | `absolute_max` | The hard cap on submitted rows, including rows Django must reject before cleaning. |
442
+
443
+ ### `SetField`
444
+
445
+ A `SequenceField` which returns a set and keeps each cleaned value once.
446
+
447
+ | Parameter | What it does |
448
+ | --- | --- |
449
+ | `child_field` | The Django field repeated for every row. |
450
+ | `min_length` | The fewest cleaned values allowed after duplicates are removed. |
451
+ | `max_length` | The most cleaned values allowed after duplicates are removed. |
452
+ | `absolute_max` | The hard cap on submitted rows, including rows Django must reject before cleaning. |
453
+
454
+ ### `FrozenSetField`
455
+
456
+ It's the same as `SetField`, but giving back an immutable `frozenset` instead.
457
+
458
+ | Parameter | What it does |
459
+ | --- | --- |
460
+ | `child_field` | The Django field repeated for every row. |
461
+ | `min_length` | The fewest cleaned values allowed after duplicates are removed. |
462
+ | `max_length` | The most cleaned values allowed after duplicates are removed. |
463
+ | `absolute_max` | The hard cap on submitted rows, including rows Django must reject before cleaning. |
464
+
465
+
466
+ ## Design notes
467
+
468
+ ### The render patch
469
+
470
+ Django knows whether `form.as_p()` or `form.as_table()` is rendering a form,
471
+ but it does not tell the widget. The app installs a small wrapper around
472
+ `BaseForm.render`, `__str__`, and `__html__` so composite wrappers can choose
473
+ the matching layout. Custom form templates go straight through Django's usual
474
+ rendering path.
475
+
476
+ ### On set behaviour...
477
+
478
+ `SetField` and `FrozenSetField` turn cleaned rows into a set before checking
479
+ the length. Two `42`s are one value, not two. They also compare membership
480
+ without caring about row order: the same set in a different order is still the
481
+ same set.
482
+
483
+ ### JavaScript helpers for `SequenceField`
484
+
485
+ It emits bubbling
486
+ `nestingdolls:sequence-ready`, cancelable `nestingdolls:sequence-add` and
487
+ `nestingdolls:sequence-remove`, and after-the-fact
488
+ `nestingdolls:sequence-change` events. You should listen for those and make
489
+ something happen that that fits your page, maybe it's a toast or counter or
490
+ something? I dunno!
491
+
492
+ `preventDefault()` stops an add or removal.
493
+
494
+ If your page replaces sequence markup after it loads, dispatch
495
+ `new Event("nestingdolls:sequence-enhance")` on `document` after insertion.
496
+ The helpers rescan the document and enhance the new sequence widgets.
497
+