|
1 | 1 | # django-formset |
2 | | -## Better User Experience for Django Forms |
3 | 2 |
|
4 | | -This library handles single forms and collections of forms with a way better user experience than |
5 | | -the internal Django implementation for |
6 | | -[formsets](https://docs.djangoproject.com/en/stable/topics/forms/formsets/) offers. |
| 3 | +`django-formset` delivers modern form handling for Django projects, combining the framework’s familiar server-side patterns with polished client-side experiences. It renders individual forms, formsets, and nested collections, wrapping them in a web component that keeps user interactions fast, accessible, and consistent across devices. |
7 | 4 |
|
| 5 | +## Why It Matters |
8 | 6 |
|
9 | | -### News: |
| 7 | +- Present forms with layouts tailored to popular CSS frameworks without rewriting templates. |
| 8 | +- Validate inputs immediately in the browser using the same rules declared on the Python side. |
| 9 | +- Offload heavy interactions—file uploads, large select lists, nested collections—to purpose-built widgets. |
| 10 | +- Keep Django admin users and public-facing visitors on the same UX foundation. |
| 11 | +- Ship a single TypeScript-powered bundle, no external JavaScript dependencies required. |
10 | 12 |
|
11 | | -Starting with version 2, developers can use `ModelForm`s and `FormCollection`s in the django-admin. |
12 | | -In version 2 it also is possible to store the content of multiple form fields or even an entire form |
13 | | -collection inside a `JSONField`. Make sure to read the |
14 | | -[Changelog](https://github.com/jrief/django-formset/blob/releases/2.0/CHANGELOG.md) for version 2 |
15 | | -before proceeding. |
| 13 | +## Installation |
16 | 14 |
|
17 | | -[](https://django-formset.fly.dev/) |
18 | | -[](https://github.com/jrief/django-formset/actions) |
19 | | -[](https://pypi.python.org/pypi/django-formset) |
20 | | -[](https://pypi.python.org/pypi/django-formset) |
21 | | -[](https://pypi.python.org/pypi/django-formset) |
22 | | -[](https://github.com/jrief/django-formset/blob/master/LICENSE) |
23 | | - |
24 | | -**Before upgrading to version 2.0, please read the [Changelog](CHANGELOG.md).** |
| 15 | +```bash |
| 16 | +pip install django-formset |
| 17 | +``` |
25 | 18 |
|
26 | | -Let's explain it using a short example. Say, we have a Django form with three fields: |
| 19 | +Activate the renderer and the app: |
27 | 20 |
|
28 | 21 | ```python |
29 | | -from django.forms import fields, forms |
| 22 | +# settings.py |
| 23 | +INSTALLED_APPS = [ |
| 24 | + # ... |
| 25 | + "django_formset", |
| 26 | +] |
30 | 27 |
|
31 | | -class AddressForm(forms.Form): |
32 | | - recipient = fields.CharField(label="Recipient") |
33 | | - postal_code = fields.CharField(label="Postal Code") |
34 | | - city = fields.CharField(label="City") |
| 28 | +FORM_RENDERER = "formset.renderers.FormsetRenderer" |
35 | 29 | ``` |
36 | 30 |
|
37 | | -After creating a |
38 | | -[Django FormView](https://docs.djangoproject.com/en/stable/ref/class-based-views/generic-editing/#django.views.generic.edit.FormView) |
39 | | -we can render the above form using a slightly modified template: |
| 31 | +Collect static assets before deployment: |
40 | 32 |
|
41 | | -```html |
42 | | -{% load formsetify %} |
43 | | -{% render_form form "bootstrap" %} |
| 33 | +```bash |
| 34 | +python manage.py collectstatic |
44 | 35 | ``` |
45 | 36 |
|
46 | | -This will render our form using the layout and CSS classes as proposed by |
47 | | -[Bootstrap's style guide](https://getbootstrap.com/docs/5.1/forms/overview/): |
48 | | - |
49 | | - |
| 37 | +## First Form |
50 | 38 |
|
51 | | -Or if rendered with alternative CSS classes: |
52 | | - |
53 | | -```html |
54 | | -{% load formsetify %} |
55 | | -{% render_form form "bootstrap" field_css_classes="row mb-3" label_css_classes="col-sm-3" control_css_classes="col-sm-9" %} |
| 39 | +```python |
| 40 | +# forms.py |
| 41 | +from django import forms |
| 42 | +from formset.widgets import UploadedFileInput |
| 43 | + |
| 44 | +class ContactForm(forms.Form): |
| 45 | + name = forms.CharField(label="Full name") |
| 46 | + email = forms.EmailField() |
| 47 | + resume = forms.FileField(widget=UploadedFileInput(), required=False) |
| 48 | + message = forms.CharField(widget=forms.Textarea) |
56 | 49 | ``` |
57 | 50 |
|
58 | | - |
59 | | - |
60 | | - |
61 | | -Or if rendered with the Tailwind renderer: |
62 | | - |
63 | | -```html |
64 | | -{% load formsetify %} |
65 | | -{% render_form form "tailwind" %} |
| 51 | +```python |
| 52 | +# views.py |
| 53 | +from django.views.generic import FormView |
| 54 | +from formset.views import FormSetMixin |
| 55 | +from .forms import ContactForm |
| 56 | + |
| 57 | +class ContactView(FormSetMixin, FormView): |
| 58 | + template_name = "contact/form.html" |
| 59 | + form_class = ContactForm |
| 60 | + success_url = "/contact/thanks/" |
66 | 61 | ``` |
67 | 62 |
|
68 | | - |
69 | | - |
70 | | -**django-formset** provides form renderers for all major CSS frameworks, such as |
71 | | -[Bootstrap 5](https://getbootstrap.com/docs/5.1/forms/overview/), |
72 | | -[Bulma](https://bulma.io/documentation/form/general/), |
73 | | -[Foundation 6](https://get.foundation/sites/docs/forms.html), |
74 | | -[Tailwind](https://tailwindcss.com/) and [UIkit](https://getuikit.com/). |
75 | | - |
76 | | - |
77 | | -### Multiple Input Widgets |
78 | | - |
79 | | -Furthermore, it can render all widgets provided by Django. This includes [multiple checkboxes](https://docs.djangoproject.com/en/stable/ref/forms/widgets/#checkboxselectmultiple) |
80 | | -and radio selects, even with multiple option groups: |
81 | | - |
82 | | - |
83 | | - |
84 | | - |
85 | | -### File Uploading Widget |
86 | | - |
87 | | -Uploading files is performed asynchronously, separating the payload upload from its form submission. |
88 | | -It provides a drag-and-drop widget plus a file select button. This allows to preview uploaded files |
89 | | -before form submission. It also makes the submission much faster, because the file is already in a |
90 | | -temporary location on the server. |
91 | | - |
92 | | -| Empty file upload | Pending file upload | |
93 | | -|-------------------------------------------|-------------------------------------| |
94 | | -|  |  | |
95 | | - |
96 | | - |
97 | | -### Alternatives for `<select>` and `<select multiple>` Widgets |
98 | | - |
99 | | -The default HTML `<select>` widget can be replaced by a counterpart with autocompletion. No extra |
100 | | -endpoint is required, because that's handled by the Django view already controlling the form. |
101 | | - |
102 | | -The default HTML `<select multiple="multiple">` widget can be replaced by two different widgets, one |
103 | | -which keeps the selected options inlined, and one which keeps them inside a "select-from" and a |
104 | | -"selected option" field. |
105 | | - |
106 | | -| Multi Select with autocomplete | Multi Select with source and target | |
107 | | -|---------------------------------------|-------------------------------------------| |
108 | | -|  |  | |
109 | | - |
110 | | -Similar widgets can be found in the Django admin to make many-to-many relations editable. In |
111 | | -**django-formset**, the right widget (with source and target) offers some additional features: |
112 | | - |
113 | | -* It can handle relations where the source contains too many entries to be loaded once. Instead, |
114 | | - this widget queries the database when searching for an option. It uses the same autocomplete |
115 | | - endpoint. |
116 | | -* The right part of the widget can be filtered as well. |
117 | | -* The widget has a redo/undo functionality in case the user mistakenly selected wrong option(s). |
118 | | -* Optionally, selected options in the right part of the widget can be sorted. This order then is |
119 | | - reflected in an |
120 | | - [extra field](https://docs.djangoproject.com/en/stable/topics/db/models/#intermediary-manytomany) |
121 | | - on the many-to-many relationship. |
122 | | - |
123 | | - |
124 | | -## Button actions |
125 | | - |
126 | | -In **django-formset**, the button used for submission can hold a *chain of actions*. This for |
127 | | -instance allows to disable the button, and/or add a spinning wheel while submitting data. It also is |
128 | | -possible to specify the success page as an HTML link, rather than having it to hard-code inside the |
129 | | -Django view. There is a complete set of predefined actions to select from, when designing the submit |
130 | | -button. |
131 | | - |
132 | | - |
133 | | - |
134 | | - |
135 | | -## Immediate Form Validation |
136 | | - |
137 | | -Each field is validated as soon as it loses focus. This gives immediate feedback and signalizes if |
138 | | -some user input will not be accepted, when submitting the form. The browser side validation |
139 | | -constraints are excatly the same, as those defined for each Django field in Python. |
140 | | - |
141 | | -Not every value or combination of thereof can be validated by the browser, but instead may be |
142 | | -rejected by the backend application. For instance, the `clean()`- and/or `clean_FIELDNAME()`-methods |
143 | | -may complain about values using some kind of internal logic. |
144 | | - |
145 | | -Those serverside errors are sent back to the client and shown nearby the rejected fields without |
146 | | -having to re-render the complete page. On success, a given page is loaded (or another alternative |
147 | | -action is performed). |
148 | | - |
149 | | - |
150 | | -## Grouping Forms |
151 | | - |
152 | | -As the name "formset" suggests, **django-formset** allows to manage more than one form. It therefore |
153 | | -is possible to create collections of forms and even nest those collections into each other. |
154 | | -Collections can be declared to have siblings, allowing them to be instantiated multiple times. This |
155 | | -is similar to Django's Stacked- and Tabular-Inlines, but allows an infinite number of nesting |
156 | | -levels. Moreover, such collections with siblings can optionally be sorted. |
157 | | - |
158 | | -[](https://youtu.be/dxyzzGOeNY4) |
159 | | - |
160 | | -[watch as video](https://youtu.be/dxyzzGOeNY4) |
161 | | - |
162 | | -A form collection is also useful to create an editor for models wich have a one-to-one relation. |
163 | | -The Django admin for instance requires to use a Stacked- or Tabular-Inline, which however is |
164 | | -designed to handle one-to-many relations. With collections these two interconnected models can be |
165 | | -handled with seemingly the same form (although in the background those are separated entinties). |
166 | | - |
167 | | - |
168 | | -## Conditional hiding/disabling |
169 | | - |
170 | | -Since each formset holds its state (the current value of their fields), that information can be used |
171 | | -to conditionally hide or disable other fields or even a complete fieldset. |
172 | | - |
173 | | -By adding the special attributes `df-show="condition"`, `df-hide="condition"` or |
174 | | -`df-disable="condition"` on an input fields or on a fieldsets, one can hide or disable these marked |
175 | | -fields. This `condition` can be any expression evaluating the current field values of the formset. |
176 | | - |
177 | | -## Alternative Widgets |
178 | | - |
179 | | -**django-formset** provides alternative widgets for many fields provided by Django. For instance: |
180 | | - |
181 | | -### Widgets for Django's `DateField` |
182 | | - |
183 | | -Modern browsers provide a built-in date picker, which now can be used instead of the default |
184 | | -`<input type="text">` widget. In addition to that, **django-formset** provides custom web components |
185 | | -which adopt themselves to the chosen CSS framework. This allows to render the date picker in the |
186 | | -same style as the rest of the form. |
187 | | - |
188 | | -## Editing Richtext |
189 | | - |
190 | | -**django-formset** integrates an extendable richtext editor, based on [TipTap](https://tiptap.dev/docs). |
191 | | - |
192 | | - |
193 | | - |
194 | | -Compared to most stand-alone solutions, this editor allows to integrate custom dialog forms and |
195 | | -formsets into the editor itself. |
196 | | - |
197 | | - |
198 | | -## Mapping of Form Fields to a `JSONField` |
199 | | - |
200 | | -For unstructured data, it is possible to map multiple form fields and even collections into a |
201 | | -[`JSONField`](https://docs.djangoproject.com/en/stable/ref/models/fields/#jsonfield) in the model. |
202 | | - |
203 | | -## Admin Integration |
204 | | - |
205 | | -Forms and collections suitable for **django-formset** views can be used in the Django admin as well. |
206 | | -This allows developers to adopt the same form design and user experience as in the frontend of their |
207 | | -application. |
208 | | - |
209 | | - |
210 | | - |
211 | | - |
212 | | -## How does this all work? |
213 | | - |
214 | | -**django-formset** makes use of the |
215 | | -[form renderer](https://docs.djangoproject.com/en/stable/ref/forms/renderers/) introduced in |
216 | | -Django 4. This allows to create special renderers for each of the supported CSS frameworks. In |
217 | | -addition to the form structure proposed by those framework vendors, this library adds private HTML |
218 | | -tags to each field containg the constraint information as declared in Python. |
219 | | - |
220 | | -The form or the collections of forms then is wrapped by the provided |
221 | | -[webcomponent](https://developer.mozilla.org/en-US/docs/Web/Web_Components) `<django-formset>`. |
222 | | -The JavaScript part (actually TypeScript) making up that webcomponent then handles the form |
223 | | -validation, its submission, instantiation or removal of collection siblings, etc. |
224 | | - |
225 | | -Some of the widgets described above (select with autocomplete, file upload) also require JavaScript |
226 | | -code. The client side functionality of those widgets also is handled by that webcomponent. |
227 | | -Widgets which require autocompletion use the same endpoint as that webcomponent itself. So there is |
228 | | -no need to add extra endpoints to the URL router. |
229 | | - |
230 | | -This finally means, that an enduser must _only_ import this single JavaScript file and wrap its |
231 | | -single form or collection of forms into a single HTML element such as |
232 | | - |
233 | 63 | ```html |
234 | | -<django-formset endpoint="/path/to/myproject/view" csrf-token="…"> |
235 | | - … |
| 64 | +<!-- templates/contact/form.html --> |
| 65 | +{% load formsetify %} |
| 66 | +<django-formset endpoint="{% url 'contact' %}"> |
| 67 | + {% render_form form "bootstrap" %} |
| 68 | + <button type="submit" df-action="submit disable show-spinner">Send</button> |
236 | 69 | </django-formset> |
237 | 70 | ``` |
238 | 71 |
|
239 | | -The Django view handling the form or collection of forms requires a special mixin class but |
240 | | -otherwise is the same as those proposed by Django, for instance its |
241 | | -[FormView](https://docs.djangoproject.com/en/stable/topics/class-based-views/generic-editing/). |
242 | | - |
243 | | -The form classes can be reused unaltered, except for replacing the widgets if desired or required |
244 | | -(the `FileField` requires a different widget). |
245 | | - |
246 | | - |
247 | | -## Reference Documentation |
| 72 | +The web component serializes the form into JSON, talks to the Django view via fetch, and streams back validation errors or success actions without a full-page reload. |
248 | 73 |
|
249 | | -Reference documentation with interactive samples can be found at |
250 | | -[https://django-formset.fly.dev/](https://django-formset.fly.dev/). |
| 74 | +## Feature Tour |
251 | 75 |
|
| 76 | +- **Renderer library** for Bootstrap, Tailwind, Bulma, Foundation, and UIkit with matching HTML structure and classes. |
| 77 | +- **Widget suite** covering asynchronous uploads, autocomplete selects, dual-list selectors, date pickers, phone numbers with flags, and rich text editing. |
| 78 | +- **Action chains** to configure submit buttons with behaviors like disabling, showing spinners, redirecting, or broadcasting events. |
| 79 | +- **Nested collections** that let you add, remove, sort, and validate repeated form groups with undo/redo support. |
| 80 | +- **JSONField mapping** to persist an entire hierarchy of form data inside a relational model without custom serialization. |
| 81 | +- **Conditional visibility** using `df-show`, `df-hide`, and `df-disable` attributes that react instantly to field values. |
252 | 82 |
|
253 | | -## Motivation |
| 83 | +## Working in Django Admin |
254 | 84 |
|
255 | | -Instead of using a `<form>`-tag and include all its fields, here we wrap the complete form inside |
256 | | -the special webcomponent `<django-formset>`. It allows the client to communicate with the Django |
257 | | -view (we name this "endpoint") using the |
258 | | -[fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). |
259 | | -This means, that multiple `<form>`-elements can be wrapped into a formset. It also means, that the |
260 | | -submit `<button>` can be placed outside of the `<form>`-element. By doing so, the form's payload |
261 | | -is sent using `Content-Type: application/json` instead of the usual |
262 | | -`Content-Type: application/x-www-form-urlencoded`. By using JSON for the payload, the form data is |
263 | | -mapped into JavaScript objects and collections of forms are represented by nested data structures. |
| 85 | +Reuse your public-facing forms inside the admin by wrapping them in `FormCollection` classes and enabling the provided admin mixins. Editors gain the same asynchronous uploads, validation messaging, and layout consistency without bespoke admin templates. |
264 | 86 |
|
265 | | -**When designing this library, the main goal was to keep the programming interface a near as |
266 | | -possible to the way Django handles forms, models and views.** |
| 87 | +## Extending the Library |
267 | 88 |
|
| 89 | +- Create tailored renderers by subclassing the base renderer and overriding template fragments. |
| 90 | +- Compose additional button actions to hook into custom analytics, notifications, or navigation flows. |
| 91 | +- Augment the web component with lightweight JavaScript modules that tap into lifecycle callbacks while keeping the main bundle intact. |
268 | 92 |
|
269 | | -## Summary |
| 93 | +## Testing Tips |
270 | 94 |
|
271 | | -* Before submitting, all form fields are prevalidated by the browser, using the same constraints as |
272 | | - declared for each Django form or model field in Python. |
273 | | -* The form's data is sent by an Ajax request, preventing a full page reload. This gives a much |
274 | | - better user experience. |
275 | | -* Server side validation errors are sent back to the browser, and rendered near the rejected |
276 | | - form field. |
277 | | -* Non-field validation errors are renderer together with the form. |
278 | | -* CSRF-tokens are handled through an HTTP-Header, hence there is no need to add a hidden input field |
279 | | - to each form. |
280 | | -* Forms can be rendered for different CSS frameworks using their specific style-guides for arranging |
281 | | - HTML. Curently **django-formset** includes renderers for: |
| 95 | +- Run `python manage.py test` as usual; formset views and mixins integrate with Django’s test client. |
| 96 | +- In browser dev tools, inspect the datasets attached to form elements to debug live validation or dependency expressions. |
| 97 | +- For large data sources, wire autocomplete widgets to search endpoints that reuse your view’s permission logic. |
282 | 98 |
|
283 | | - * [Bootstrap 5](https://getbootstrap.com/docs/5.0/forms/overview/), |
284 | | - * [Bulma](https://bulma.io/documentation/form/general/), |
285 | | - * [Foundation 6](https://get.foundation/sites/docs/forms.html), |
286 | | - * [Tailwind](https://tailwindcss.com/) [^1] |
287 | | - * [UIKit](https://getuikit.com/docs/form) |
| 99 | +## Resources |
288 | 100 |
|
289 | | - It usually takes about 50 lines of code to create a renderer and most widgets can even be rendered |
290 | | - using the default template as provided by Django. |
291 | | -* No external JavaScript dependencies are required. The client part is written in pure TypeScript |
292 | | - and compiles to a single, portable JS-file. |
293 | | -* Support for all standard widgets Django currently offers (except GeoSpacials). |
294 | | -* File uploads are handled asynchronously, separating the payload upload from the form submission. |
295 | | -* Select boxes with too many entries, can be filtered by the server using a search query. |
296 | | -* Radio buttons and multiple checkboxes with only a few fields can be rendered inlined rather than |
297 | | - beneath each other. |
298 | | -* The submit button(s) can be configured as a chain of actions. |
299 | | -* A formset can group multiple forms into a collection. Collections can be nested. On submission, |
300 | | - the data from this form or collection of forms is sent to the server as a group a separate |
301 | | - entities. |
302 | | -* Such a form-collection can be declared to have a list siblings, which can be changed in length |
303 | | - using one "Add" and multiple "Remove" buttons. |
304 | | -* Form fields or fieldsets can be hidden or disabled using a Boolean expression as condition. |
305 | | -* Special widgets for date and datetime fields, they can also be used for ranges. |
306 | | -* Special widget for phone numbers and countries displaying their flags. |
307 | | -* Special widget for editing Richtext. |
| 101 | +Interactive documentation, API references, and upgrade guides live at https://django-formset.fly.dev/. |
308 | 102 |
|
309 | | -[^1]: Tailwind is special here, since it doesn't include purpose-built form control classes out of |
310 | | - the box. Instead, **django-formset** offers an opinionated set of CSS classes suitable for |
311 | | - Tailwind. |
0 commit comments