Skip to content

Commit 88f40f7

Browse files
author
Juan Pablo Manson
committed
Enhance README.md with detailed explanations, examples, and compatibility guarantees
1 parent 605c968 commit 88f40f7

1 file changed

Lines changed: 170 additions & 6 deletions

File tree

README.md

Lines changed: 170 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,68 @@
1+
<div align="center">
2+
13
# codeforms
24

3-
A Python library for dynamically creating, validating, and rendering web forms using [Pydantic](https://docs.pydantic.dev/).
5+
**Forms as data. Define once in Python or JSON — validate on the backend, render on the frontend.**
6+
7+
[![PyPI version](https://img.shields.io/pypi/v/codeforms.svg)](https://pypi.org/project/codeforms/)
8+
[![Python versions](https://img.shields.io/pypi/pyversions/codeforms.svg)](https://pypi.org/project/codeforms/)
9+
[![Tests](https://github.com/jpmanson/codeforms/actions/workflows/tests.yml/badge.svg)](https://github.com/jpmanson/codeforms/actions/workflows/tests.yml)
10+
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
11+
[![Built on Pydantic v2](https://img.shields.io/badge/built%20on-Pydantic%20v2-e92063.svg)](https://docs.pydantic.dev/)
12+
13+
</div>
14+
15+
---
16+
17+
## Why codeforms?
18+
19+
Traditional form libraries (Django Forms, WTForms) define forms as **Python classes**. A new form — or a change to an existing one — means a code change, a review and a deploy.
20+
21+
codeforms defines forms as **data**: a Pydantic model that round-trips losslessly to JSON. The same definition can be stored in a database, edited by a non-developer in a form builder, versioned per tenant, and shipped at runtime — while still giving you strict server-side validation and real Pydantic models.
22+
23+
```text
24+
┌──────────────────────┐
25+
Python ──────────▶ │ │ ──────▶ HTML (plain / BS4 / BS5)
26+
│ Form │ ──────▶ JSON Schema (draft-07)
27+
JSON / DB ───────▶ │ (single source │ ──────▶ Client-side JS validation
28+
│ of truth) │ ──────▶ Pydantic model (server-side)
29+
Form builder ────▶ │ │ ──────▶ JSON / dict
30+
└──────────────────────┘
31+
```
32+
33+
### What you get
34+
35+
| | |
36+
|---|---|
37+
| 🗃️ **Forms as data** | Full JSON round-trip. Store definitions in a DB, version them, ship them at runtime — no redeploy to change a form. |
38+
| 🛡️ **One source of truth** | The same definition drives server validation, HTML rendering, JSON Schema and client-side JS checks. They cannot drift apart. |
39+
|**Pydantic v2 native** | `FormDataValidator.create_model()` builds a real Pydantic model — drops straight into FastAPI, Flask, Litestar or any typed backend. |
40+
| 🔀 **Dynamic logic built in** | Conditional visibility (`visible_when`), dependent options and multi-step wizards — declarative, serializable, and opt-in. |
41+
| 🌍 **i18n out of the box** | English and Spanish included; register any locale at runtime. Validation messages follow the active locale. |
42+
| 🧩 **Extensible by design** | Register your own field types with `register_field_type()` — no core changes, full serialization support. |
43+
| 🔗 **Interoperable** | JSON Schema draft-07 export works with [React JSON Schema Form](https://github.com/rjsf-team/react-jsonschema-form), [Angular Formly](https://formly.dev/) and any JSON Schema validator. |
44+
| 🪶 **Tiny footprint** | One dependency (`pydantic[email]`). Python 3.9+. MIT. 248 tests in CI. |
45+
46+
### Where it fits
47+
48+
- **Low-code / form builders** — persist user-designed forms as JSON and validate submissions safely.
49+
- **BPM & workflow engines** — dynamic task forms (approvals, intake, review steps) defined per process, in the spirit of [Camunda form-js](https://bpmn.io/toolkit/form-js/).
50+
- **Multi-tenant SaaS** — each customer gets a different form without a branch, a build or a deploy.
51+
- **Surveys, intake and onboarding flows** — multi-step wizards with conditional branching.
52+
- **Headless APIs** — serve `json_schema` to a React/Vue frontend and validate the same contract server-side.
53+
54+
### Compared to
55+
56+
| | codeforms | Django Forms / WTForms | Pydantic alone | React JSON Schema Form |
57+
|---|---|---|---|---|
58+
| Runtime-definable (no deploy) |||||
59+
| Server-side validation |||||
60+
| HTML rendering |||| ✅ (frontend only) |
61+
| JSON Schema export ||| ⚠️ partial | — (consumes it) |
62+
| Conditional visibility / wizards |||| ⚠️ limited |
63+
| Framework-agnostic |||||
64+
65+
---
466

567
## Installation
668

@@ -18,7 +80,7 @@ Requires Python 3.9+.
1880

1981
## Quick Start
2082

21-
### Creating a Form
83+
### 1. Define a form
2284

2385
Everything starts with the `Form` class. A form is defined with a name and a list of fields.
2486

@@ -30,22 +92,50 @@ form = Form(
3092
fields=[
3193
TextField(name="full_name", label="Full Name", required=True),
3294
EmailField(name="email", label="Email", required=True),
33-
NumberField(name="age", label="Age"),
95+
NumberField(name="age", label="Age", min_value=18),
3496
]
3597
)
3698
```
3799

100+
### 2. Validate submitted data
101+
102+
```python
103+
from codeforms import FormDataValidator
104+
105+
Model = FormDataValidator.create_model(form)
106+
user = Model.model_validate({"full_name": "Ada", "email": "ada@example.com", "age": 36})
107+
```
108+
109+
### 3. Render it
110+
111+
```python
112+
html = form.export("html_bootstrap5", submit=True)["output"]
113+
schema = form.export("json_schema")["output"]
114+
```
115+
116+
### 4. Store it and bring it back
117+
118+
```python
119+
raw = form.to_json() # persist anywhere: DB, S3, a config repo
120+
form = Form.loads(raw) # lossless round-trip, including custom field types
121+
```
122+
123+
That last step is the whole point: the form is data, not code.
124+
38125
### The `Form` Class
39126

40127
The `Form` class is the main container for your form structure.
41128

42129
- `id` — Auto-generated UUID.
43130
- `name` — Form name (used in HTML export and validation).
44-
- `fields` — A list of field objects (e.g. `TextField`, `EmailField`).
131+
- `fields` — A list of field objects (e.g. `TextField`, `EmailField`). Always returns a **flat** list, even for grouped or stepped forms.
132+
- `content` — The structured list of fields, `FieldGroup`s and `FormStep`s.
45133
- `css_classes` — Optional CSS classes for the `<form>` tag.
46134
- `version` — Form version number.
47135
- `attributes` — Dictionary of additional HTML attributes for the `<form>` tag.
48136

137+
> **Backward compatibility:** `Form` accepts both `fields=` and `content=`. Legacy payloads load unchanged, and `.fields` always gives you the flat view.
138+
49139
## Field Types
50140

51141
All fields inherit from `FormFieldBase` and share these common attributes:
@@ -60,6 +150,7 @@ All fields inherit from `FormFieldBase` and share these common attributes:
60150
- `css_classes` — CSS classes for the field element.
61151
- `readonly` — Whether the field is read-only.
62152
- `attributes` — Additional HTML attributes for the `<input>` tag.
153+
- `visible_when` — Optional list of `VisibilityRule` (see [Conditional Visibility](#conditional-visibility)).
63154

64155
### Available Fields
65156

@@ -76,6 +167,7 @@ All fields inherit from `FormFieldBase` and share these common attributes:
76167
- `options`: List of `SelectOption(value="...", label="...")`.
77168
- `multiple`: Enables multi-select.
78169
- `min_selected`, `max_selected`: Selection count limits (multi-select only).
170+
- `dependent_options`: See [Dependent Options](#dependent-options).
79171
- **`RadioField`** — Radio buttons (`<input type="radio">`).
80172
- `options`: List of `SelectOption`.
81173
- `inline`: Display options inline.
@@ -87,13 +179,26 @@ All fields inherit from `FormFieldBase` and share these common attributes:
87179
- `accept`: Accepted file types (e.g. `"image/*,.pdf"`).
88180
- `multiple`: Allow multiple file uploads.
89181
- **`HiddenField`** — Hidden field (`<input type="hidden">`).
182+
- **`UrlField`** — URL input (`<input type="url">`).
183+
- `minlength`, `maxlength`: Min/max text length.
184+
- **`TextareaField`** — Multi-line text (`<textarea>`).
185+
- `rows`, `minlength`, `maxlength`.
90186
- **`ListField`** — Array of primitive values.
91187
- `item_type`: Primitive type for each item (`text`, `number`, `email`, `url`, `date`).
92188
- `min_items`, `max_items`: List size limits.
93189
- **`ObjectListField`** — Array of homogeneous objects validated against nested subfields.
94190
- `fields`: List of subfields that define each object shape.
95191
- `min_items`, `max_items`: List size limits.
96192

193+
### Containers
194+
195+
- **`FieldGroup`** — Groups fields into a titled section (`<fieldset>` / `<legend>` in HTML export).
196+
- `title`, `description`, `fields`.
197+
- `collapsible`, `collapsed`: Collapsible section metadata for your frontend.
198+
- **`FormStep`** — A wizard step; see [Multi-Step Wizard Forms](#multi-step-wizard-forms).
199+
200+
Fields inside containers are always reachable through the flat `form.fields` view.
201+
97202
### `ObjectListField`
98203

99204
Use `ObjectListField` when a form needs a repeatable list of structured rows, such as parallel approvers, attendees with roles, or line items.
@@ -234,7 +339,7 @@ print(dict_output)
234339
| `json` | JSON representation of the form |
235340
| `dict` | Python dictionary representation |
236341

237-
HTML export can also generate a `<script>` block for basic client-side validation.
342+
HTML export can also generate a `<script>` block for basic client-side validation, derived from the same field constraints used on the server — so the two can't drift apart. All rendered values are HTML-escaped.
238343

239344
### JSON Schema Export
240345

@@ -444,6 +549,8 @@ See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working exampl
444549

445550
## Dynamic Forms
446551

552+
All dynamic behavior below is **opt-in and additive**: forms without dynamic metadata behave exactly as before, and every new JSON key is optional.
553+
447554
### Conditional Visibility
448555

449556
Fields can be shown or hidden based on the value of other fields using `visible_when`. This is metadata that your frontend can use for dynamic UI, and the backend can respect during validation.
@@ -657,6 +764,63 @@ for name, classes in sorted(get_registered_field_types().items()):
657764

658765
See [`examples/custom_fields.py`](examples/custom_fields.py) for a full working example.
659766

767+
---
768+
769+
## Compatibility Guarantees
770+
771+
codeforms takes backward compatibility seriously:
772+
773+
- Existing usage — `Form(fields=[...])`, `form.fields`, `validate_data()`, `validate_form_data()`, current HTML output — keeps working without code changes.
774+
- New schema keys are **additive and optional**; previously serialized forms load unchanged.
775+
- Dynamic behavior (visibility, dependent options, wizards) is **disabled by default** and enabled explicitly.
776+
- Legacy paths get a deprecation warning for at least one minor release before removal in a major release.
777+
778+
## Examples
779+
780+
| Example | Shows |
781+
|---|---|
782+
| [`basic_usage.py`](examples/basic_usage.py) | Defining, validating and exporting a form |
783+
| [`conditional_visibility.py`](examples/conditional_visibility.py) | `visible_when` rules and dynamic validation |
784+
| [`dependent_options.py`](examples/dependent_options.py) | Option sets driven by another field |
785+
| [`wizard_form.py`](examples/wizard_form.py) | Multi-step forms and per-step validation |
786+
| [`custom_fields.py`](examples/custom_fields.py) | Registering your own field types |
787+
| [`i18n_usage.py`](examples/i18n_usage.py) | Locales and custom translations |
788+
789+
## Roadmap
790+
791+
- [x] Internationalization (`en` / `es` / runtime-registered locales)
792+
- [x] Custom field type registry
793+
- [x] Conditional visibility, dependent options, multi-step wizards
794+
- [x] JSON Schema (draft-07) export
795+
- [ ] Frontend-friendly dict export optimized for React/Vue
796+
- [ ] Richer HTML export: `aria-*` accessibility attributes, Tailwind support
797+
- [ ] FastAPI integration (`FormDependency`)
798+
- [ ] Django bridge (convert to/from a Django `Form`)
799+
- [ ] Import **from** JSON Schema and from Camunda / form-js schemas
800+
- [ ] PDF export
801+
802+
See [`todo/TODO.md`](todo/TODO.md) for the detailed plan.
803+
804+
## Development
805+
806+
```bash
807+
# Install in editable mode with dev extras
808+
pip install -e ".[dev]"
809+
810+
# Run the test suite
811+
pytest -q
812+
813+
# Lint and format
814+
ruff check src/ tests/ examples/ --fix
815+
ruff format src/ tests/ examples/
816+
```
817+
818+
Tests run on Python 3.11 and 3.12 in CI.
819+
820+
## Contributing
821+
822+
Issues and pull requests are welcome. Please add tests for any behavior change, and keep the compatibility guarantees above in mind.
823+
660824
## License
661825

662-
MIT
826+
MIT — see [LICENSE](LICENSE).

0 commit comments

Comments
 (0)