codeforms 0.2.0__tar.gz → 0.2.1__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 (32) hide show
  1. {codeforms-0.2.0 → codeforms-0.2.1}/.gitignore +2 -0
  2. {codeforms-0.2.0 → codeforms-0.2.1}/PKG-INFO +259 -1
  3. {codeforms-0.2.0 → codeforms-0.2.1}/README.md +257 -0
  4. {codeforms-0.2.0 → codeforms-0.2.1}/examples/basic_usage.py +7 -5
  5. {codeforms-0.2.0 → codeforms-0.2.1}/examples/conditional_visibility.py +4 -2
  6. {codeforms-0.2.0 → codeforms-0.2.1}/examples/custom_fields.py +7 -4
  7. {codeforms-0.2.0 → codeforms-0.2.1}/examples/dependent_options.py +2 -1
  8. {codeforms-0.2.0 → codeforms-0.2.1}/examples/i18n_usage.py +25 -17
  9. {codeforms-0.2.0 → codeforms-0.2.1}/examples/wizard_form.py +31 -23
  10. codeforms-0.2.1/pyproject.toml +49 -0
  11. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/__init__.py +23 -22
  12. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/export.py +325 -101
  13. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/fields.py +58 -37
  14. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/forms.py +327 -207
  15. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/i18n.py +1 -30
  16. {codeforms-0.2.0 → codeforms-0.2.1}/src/codeforms/registry.py +41 -18
  17. {codeforms-0.2.0 → codeforms-0.2.1}/tests/conftest.py +0 -1
  18. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_basic_usage.py +6 -2
  19. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_dependent_options.py +11 -9
  20. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_dynamic_visibility.py +39 -20
  21. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_i18n.py +18 -13
  22. codeforms-0.2.1/tests/test_json_schema_export.py +640 -0
  23. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_phase2_compat.py +9 -13
  24. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_registry.py +54 -29
  25. {codeforms-0.2.0 → codeforms-0.2.1}/tests/test_wizard_steps.py +25 -18
  26. codeforms-0.2.0/pyproject.toml +0 -25
  27. {codeforms-0.2.0 → codeforms-0.2.1}/.github/workflows/python-publish.yml +0 -0
  28. {codeforms-0.2.0 → codeforms-0.2.1}/.github/workflows/tests.yml +0 -0
  29. {codeforms-0.2.0 → codeforms-0.2.1}/.python-version +0 -0
  30. {codeforms-0.2.0 → codeforms-0.2.1}/LICENSE +0 -0
  31. {codeforms-0.2.0 → codeforms-0.2.1}/tests/__init__.py +0 -0
  32. {codeforms-0.2.0 → codeforms-0.2.1}/uv.lock +0 -0
@@ -15,6 +15,8 @@ info
15
15
  # Agents
16
16
  CLAUDE.md
17
17
  .claude
18
+ .agents
19
+ knowledge.md
18
20
 
19
21
  # Caches and temps
20
22
  .uv-cache
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeforms
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Python library for creating, validating, and rendering web forms using Pydantic
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -8,6 +8,7 @@ Requires-Python: >=3.9
8
8
  Requires-Dist: pydantic[email]>=2.0
9
9
  Provides-Extra: dev
10
10
  Requires-Dist: pytest>=8.0; extra == 'dev'
11
+ Requires-Dist: ruff>=0.8; extra == 'dev'
11
12
  Description-Content-Type: text/markdown
12
13
 
13
14
  # codeforms
@@ -179,11 +180,115 @@ print(dict_output)
179
180
  | `html` | Semantic HTML |
180
181
  | `html_bootstrap4` | HTML with Bootstrap 4 classes |
181
182
  | `html_bootstrap5` | HTML with Bootstrap 5 classes |
183
+ | `json_schema` | [JSON Schema](http://json-schema.org/draft-07/schema#) (draft-07) |
182
184
  | `json` | JSON representation of the form |
183
185
  | `dict` | Python dictionary representation |
184
186
 
185
187
  HTML export can also generate a `<script>` block for basic client-side validation.
186
188
 
189
+ ### JSON Schema Export
190
+
191
+ Generate a standard [JSON Schema (draft-07)](http://json-schema.org/draft-07/schema#) from any form. The resulting schema is compatible with tools like [React JSON Schema Form](https://github.com/rjsf-team/react-jsonschema-form), [Angular Formly](https://formly.dev/), and any JSON Schema validator.
192
+
193
+ ```python
194
+ import json
195
+ from codeforms import (
196
+ Form, TextField, EmailField, NumberField, SelectField, SelectOption,
197
+ CheckboxField, form_to_json_schema,
198
+ )
199
+
200
+ form = Form(
201
+ name="registration",
202
+ fields=[
203
+ TextField(name="name", label="Full Name", required=True, minlength=2, maxlength=100),
204
+ EmailField(name="email", label="Email", required=True),
205
+ NumberField(name="age", label="Age", min_value=18, max_value=120),
206
+ SelectField(
207
+ name="country",
208
+ label="Country",
209
+ required=True,
210
+ options=[
211
+ SelectOption(value="us", label="United States"),
212
+ SelectOption(value="uk", label="United Kingdom"),
213
+ ],
214
+ ),
215
+ CheckboxField(name="terms", label="Accept Terms", required=True),
216
+ ],
217
+ )
218
+
219
+ # Option 1: Direct function call
220
+ schema = form_to_json_schema(form)
221
+ print(json.dumps(schema, indent=2))
222
+
223
+ # Option 2: Via form.export()
224
+ result = form.export("json_schema")
225
+ schema = result["output"]
226
+ ```
227
+
228
+ Output:
229
+
230
+ ```json
231
+ {
232
+ "$schema": "http://json-schema.org/draft-07/schema#",
233
+ "type": "object",
234
+ "title": "registration",
235
+ "properties": {
236
+ "name": {
237
+ "type": "string",
238
+ "minLength": 2,
239
+ "maxLength": 100,
240
+ "title": "Full Name"
241
+ },
242
+ "email": {
243
+ "type": "string",
244
+ "format": "email",
245
+ "title": "Email"
246
+ },
247
+ "age": {
248
+ "type": "number",
249
+ "minimum": 18,
250
+ "maximum": 120,
251
+ "title": "Age"
252
+ },
253
+ "country": {
254
+ "type": "string",
255
+ "enum": ["us", "uk"],
256
+ "title": "Country"
257
+ },
258
+ "terms": {
259
+ "type": "boolean",
260
+ "title": "Accept Terms"
261
+ }
262
+ },
263
+ "required": ["name", "email", "country", "terms"],
264
+ "additionalProperties": false
265
+ }
266
+ ```
267
+
268
+ #### Field Type Mapping
269
+
270
+ | codeforms Field | JSON Schema Type | Extra Keywords |
271
+ |---|---|---|
272
+ | `TextField` | `string` | `minLength`, `maxLength`, `pattern` |
273
+ | `EmailField` | `string` (`format: "email"`) | — |
274
+ | `NumberField` | `number` | `minimum`, `maximum`, `multipleOf` |
275
+ | `DateField` | `string` (`format: "date"`) | — |
276
+ | `SelectField` | `string` + `enum` | — |
277
+ | `SelectField` (`multiple=True`) | `array` of `enum` strings | `minItems`, `maxItems`, `uniqueItems` |
278
+ | `RadioField` | `string` + `enum` | — |
279
+ | `CheckboxField` | `boolean` | — |
280
+ | `CheckboxGroupField` | `array` of `enum` strings | `uniqueItems` |
281
+ | `FileField` | `string` (`contentEncoding: "base64"`) | — |
282
+ | `FileField` (`multiple=True`) | `array` of base64 strings | — |
283
+ | `HiddenField` | `string` | — |
284
+ | `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
285
+ | `TextareaField` | `string` | `minLength`, `maxLength` |
286
+ | `ListField` | `array` | `minItems`, `maxItems` |
287
+
288
+ Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
289
+
290
+ Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
291
+
187
292
  ## Internationalization (i18n)
188
293
 
189
294
  All validation and export messages are locale-aware. **English** (`en`) and **Spanish** (`es`) are included out of the box, and you can register any additional language at runtime via `register_locale()`.
@@ -258,6 +363,159 @@ print(result["errors"][0]["message"]) # "El campo name es requerido"
258
363
 
259
364
  See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working example.
260
365
 
366
+ ## Dynamic Forms
367
+
368
+ ### Conditional Visibility
369
+
370
+ 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.
371
+
372
+ ```python
373
+ from codeforms import Form, TextField, SelectField, SelectOption, VisibilityRule
374
+
375
+ form = Form(
376
+ name="address",
377
+ fields=[
378
+ SelectField(
379
+ name="country",
380
+ label="Country",
381
+ required=True,
382
+ options=[
383
+ SelectOption(value="US", label="United States"),
384
+ SelectOption(value="AR", label="Argentina"),
385
+ ],
386
+ ),
387
+ TextField(
388
+ name="state",
389
+ label="State",
390
+ required=True,
391
+ visible_when=[
392
+ VisibilityRule(field="country", operator="equals", value="US"),
393
+ ],
394
+ ),
395
+ TextField(
396
+ name="province",
397
+ label="Province",
398
+ required=True,
399
+ visible_when=[
400
+ VisibilityRule(field="country", operator="equals", value="AR"),
401
+ ],
402
+ ),
403
+ ],
404
+ )
405
+ ```
406
+
407
+ Supported operators: `equals`, `not_equals`, `in`, `not_in`, `gt`, `lt`, `is_empty`, `is_not_empty`.
408
+
409
+ #### Dynamic Validation
410
+
411
+ Use `validate_form_data_dynamic()` to validate only the fields that are currently visible:
412
+
413
+ ```python
414
+ from codeforms import validate_form_data_dynamic
415
+
416
+ result = validate_form_data_dynamic(
417
+ form,
418
+ {"country": "US", "state": "California"},
419
+ respect_visibility=True,
420
+ )
421
+ print(result["success"]) # True — "province" is hidden, so not required
422
+ ```
423
+
424
+ The legacy `validate_form_data()` function is unchanged and always validates all fields regardless of visibility.
425
+
426
+ #### Checking Visible Fields
427
+
428
+ ```python
429
+ visible = form.get_visible_fields({"country": "US"})
430
+ print([f.name for f in visible]) # ["country", "state"]
431
+ ```
432
+
433
+ See [`examples/conditional_visibility.py`](examples/conditional_visibility.py) for a full working example.
434
+
435
+ ### Dependent Options
436
+
437
+ Use `DependentOptionsConfig` to define option sets that change based on another field's value:
438
+
439
+ ```python
440
+ from codeforms import SelectField, SelectOption, DependentOptionsConfig
441
+
442
+ city_field = SelectField(
443
+ name="city",
444
+ label="City",
445
+ options=[ # all possible options (for static HTML rendering)
446
+ SelectOption(value="nyc", label="New York City"),
447
+ SelectOption(value="bsas", label="Buenos Aires"),
448
+ ],
449
+ dependent_options=DependentOptionsConfig(
450
+ depends_on="country",
451
+ options_map={
452
+ "US": [SelectOption(value="nyc", label="New York City")],
453
+ "AR": [SelectOption(value="bsas", label="Buenos Aires")],
454
+ },
455
+ ),
456
+ )
457
+ ```
458
+
459
+ The `dependent_options` metadata serializes to JSON for your frontend to consume. See [`examples/dependent_options.py`](examples/dependent_options.py).
460
+
461
+ ## Multi-Step Wizard Forms
462
+
463
+ Use `FormStep` to split a form into multiple steps. Each step contains its own fields and can be validated independently.
464
+
465
+ ```python
466
+ from codeforms import Form, FormStep, TextField, EmailField, CheckboxField
467
+
468
+ form = Form(
469
+ name="registration",
470
+ content=[
471
+ FormStep(
472
+ title="Personal Information",
473
+ description="Tell us about yourself",
474
+ content=[
475
+ TextField(name="name", label="Name", required=True),
476
+ EmailField(name="email", label="Email", required=True),
477
+ ],
478
+ ),
479
+ FormStep(
480
+ title="Confirmation",
481
+ content=[
482
+ CheckboxField(name="terms", label="I accept the terms", required=True),
483
+ ],
484
+ validation_mode="on_submit",
485
+ ),
486
+ ],
487
+ )
488
+ ```
489
+
490
+ ### Step Validation
491
+
492
+ ```python
493
+ # Validate a single step
494
+ result = form.validate_step(0, {"name": "John", "email": "john@example.com"})
495
+ print(result["success"]) # True
496
+
497
+ # Validate all steps at once
498
+ result = form.validate_all_steps({
499
+ "name": "John",
500
+ "email": "john@example.com",
501
+ "terms": True,
502
+ })
503
+ print(result["success"]) # True
504
+ ```
505
+
506
+ ### Wizard Helpers
507
+
508
+ ```python
509
+ steps = form.get_steps() # List[FormStep]
510
+ fields = form.fields # Flat list of all fields across all steps
511
+ ```
512
+
513
+ ### HTML Export
514
+
515
+ Wizard forms export with `data-wizard="true"` on the `<form>` tag. Each step renders as a `<section data-step="true">` (not `<fieldset>`), so you can wire up your own step navigation in JavaScript.
516
+
517
+ See [`examples/wizard_form.py`](examples/wizard_form.py) for a full working example.
518
+
261
519
  ## Custom Field Types
262
520
 
263
521
  You can create your own field types by subclassing `FormFieldBase` and registering them with `register_field_type()`. Custom fields integrate seamlessly with forms, JSON serialization, validation, and HTML export.
@@ -167,11 +167,115 @@ print(dict_output)
167
167
  | `html` | Semantic HTML |
168
168
  | `html_bootstrap4` | HTML with Bootstrap 4 classes |
169
169
  | `html_bootstrap5` | HTML with Bootstrap 5 classes |
170
+ | `json_schema` | [JSON Schema](http://json-schema.org/draft-07/schema#) (draft-07) |
170
171
  | `json` | JSON representation of the form |
171
172
  | `dict` | Python dictionary representation |
172
173
 
173
174
  HTML export can also generate a `<script>` block for basic client-side validation.
174
175
 
176
+ ### JSON Schema Export
177
+
178
+ Generate a standard [JSON Schema (draft-07)](http://json-schema.org/draft-07/schema#) from any form. The resulting schema is compatible with tools like [React JSON Schema Form](https://github.com/rjsf-team/react-jsonschema-form), [Angular Formly](https://formly.dev/), and any JSON Schema validator.
179
+
180
+ ```python
181
+ import json
182
+ from codeforms import (
183
+ Form, TextField, EmailField, NumberField, SelectField, SelectOption,
184
+ CheckboxField, form_to_json_schema,
185
+ )
186
+
187
+ form = Form(
188
+ name="registration",
189
+ fields=[
190
+ TextField(name="name", label="Full Name", required=True, minlength=2, maxlength=100),
191
+ EmailField(name="email", label="Email", required=True),
192
+ NumberField(name="age", label="Age", min_value=18, max_value=120),
193
+ SelectField(
194
+ name="country",
195
+ label="Country",
196
+ required=True,
197
+ options=[
198
+ SelectOption(value="us", label="United States"),
199
+ SelectOption(value="uk", label="United Kingdom"),
200
+ ],
201
+ ),
202
+ CheckboxField(name="terms", label="Accept Terms", required=True),
203
+ ],
204
+ )
205
+
206
+ # Option 1: Direct function call
207
+ schema = form_to_json_schema(form)
208
+ print(json.dumps(schema, indent=2))
209
+
210
+ # Option 2: Via form.export()
211
+ result = form.export("json_schema")
212
+ schema = result["output"]
213
+ ```
214
+
215
+ Output:
216
+
217
+ ```json
218
+ {
219
+ "$schema": "http://json-schema.org/draft-07/schema#",
220
+ "type": "object",
221
+ "title": "registration",
222
+ "properties": {
223
+ "name": {
224
+ "type": "string",
225
+ "minLength": 2,
226
+ "maxLength": 100,
227
+ "title": "Full Name"
228
+ },
229
+ "email": {
230
+ "type": "string",
231
+ "format": "email",
232
+ "title": "Email"
233
+ },
234
+ "age": {
235
+ "type": "number",
236
+ "minimum": 18,
237
+ "maximum": 120,
238
+ "title": "Age"
239
+ },
240
+ "country": {
241
+ "type": "string",
242
+ "enum": ["us", "uk"],
243
+ "title": "Country"
244
+ },
245
+ "terms": {
246
+ "type": "boolean",
247
+ "title": "Accept Terms"
248
+ }
249
+ },
250
+ "required": ["name", "email", "country", "terms"],
251
+ "additionalProperties": false
252
+ }
253
+ ```
254
+
255
+ #### Field Type Mapping
256
+
257
+ | codeforms Field | JSON Schema Type | Extra Keywords |
258
+ |---|---|---|
259
+ | `TextField` | `string` | `minLength`, `maxLength`, `pattern` |
260
+ | `EmailField` | `string` (`format: "email"`) | — |
261
+ | `NumberField` | `number` | `minimum`, `maximum`, `multipleOf` |
262
+ | `DateField` | `string` (`format: "date"`) | — |
263
+ | `SelectField` | `string` + `enum` | — |
264
+ | `SelectField` (`multiple=True`) | `array` of `enum` strings | `minItems`, `maxItems`, `uniqueItems` |
265
+ | `RadioField` | `string` + `enum` | — |
266
+ | `CheckboxField` | `boolean` | — |
267
+ | `CheckboxGroupField` | `array` of `enum` strings | `uniqueItems` |
268
+ | `FileField` | `string` (`contentEncoding: "base64"`) | — |
269
+ | `FileField` (`multiple=True`) | `array` of base64 strings | — |
270
+ | `HiddenField` | `string` | — |
271
+ | `UrlField` | `string` (`format: "uri"`) | `minLength`, `maxLength` |
272
+ | `TextareaField` | `string` | `minLength`, `maxLength` |
273
+ | `ListField` | `array` | `minItems`, `maxItems` |
274
+
275
+ Field annotations like `label`, `help_text`, `default_value`, and `readonly` map to the JSON Schema keywords `title`, `description`, `default`, and `readOnly` respectively.
276
+
277
+ Fields inside `FieldGroup` and `FormStep` containers are flattened into the top-level `properties` automatically.
278
+
175
279
  ## Internationalization (i18n)
176
280
 
177
281
  All validation and export messages are locale-aware. **English** (`en`) and **Spanish** (`es`) are included out of the box, and you can register any additional language at runtime via `register_locale()`.
@@ -246,6 +350,159 @@ print(result["errors"][0]["message"]) # "El campo name es requerido"
246
350
 
247
351
  See [`examples/i18n_usage.py`](examples/i18n_usage.py) for a full working example.
248
352
 
353
+ ## Dynamic Forms
354
+
355
+ ### Conditional Visibility
356
+
357
+ 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.
358
+
359
+ ```python
360
+ from codeforms import Form, TextField, SelectField, SelectOption, VisibilityRule
361
+
362
+ form = Form(
363
+ name="address",
364
+ fields=[
365
+ SelectField(
366
+ name="country",
367
+ label="Country",
368
+ required=True,
369
+ options=[
370
+ SelectOption(value="US", label="United States"),
371
+ SelectOption(value="AR", label="Argentina"),
372
+ ],
373
+ ),
374
+ TextField(
375
+ name="state",
376
+ label="State",
377
+ required=True,
378
+ visible_when=[
379
+ VisibilityRule(field="country", operator="equals", value="US"),
380
+ ],
381
+ ),
382
+ TextField(
383
+ name="province",
384
+ label="Province",
385
+ required=True,
386
+ visible_when=[
387
+ VisibilityRule(field="country", operator="equals", value="AR"),
388
+ ],
389
+ ),
390
+ ],
391
+ )
392
+ ```
393
+
394
+ Supported operators: `equals`, `not_equals`, `in`, `not_in`, `gt`, `lt`, `is_empty`, `is_not_empty`.
395
+
396
+ #### Dynamic Validation
397
+
398
+ Use `validate_form_data_dynamic()` to validate only the fields that are currently visible:
399
+
400
+ ```python
401
+ from codeforms import validate_form_data_dynamic
402
+
403
+ result = validate_form_data_dynamic(
404
+ form,
405
+ {"country": "US", "state": "California"},
406
+ respect_visibility=True,
407
+ )
408
+ print(result["success"]) # True — "province" is hidden, so not required
409
+ ```
410
+
411
+ The legacy `validate_form_data()` function is unchanged and always validates all fields regardless of visibility.
412
+
413
+ #### Checking Visible Fields
414
+
415
+ ```python
416
+ visible = form.get_visible_fields({"country": "US"})
417
+ print([f.name for f in visible]) # ["country", "state"]
418
+ ```
419
+
420
+ See [`examples/conditional_visibility.py`](examples/conditional_visibility.py) for a full working example.
421
+
422
+ ### Dependent Options
423
+
424
+ Use `DependentOptionsConfig` to define option sets that change based on another field's value:
425
+
426
+ ```python
427
+ from codeforms import SelectField, SelectOption, DependentOptionsConfig
428
+
429
+ city_field = SelectField(
430
+ name="city",
431
+ label="City",
432
+ options=[ # all possible options (for static HTML rendering)
433
+ SelectOption(value="nyc", label="New York City"),
434
+ SelectOption(value="bsas", label="Buenos Aires"),
435
+ ],
436
+ dependent_options=DependentOptionsConfig(
437
+ depends_on="country",
438
+ options_map={
439
+ "US": [SelectOption(value="nyc", label="New York City")],
440
+ "AR": [SelectOption(value="bsas", label="Buenos Aires")],
441
+ },
442
+ ),
443
+ )
444
+ ```
445
+
446
+ The `dependent_options` metadata serializes to JSON for your frontend to consume. See [`examples/dependent_options.py`](examples/dependent_options.py).
447
+
448
+ ## Multi-Step Wizard Forms
449
+
450
+ Use `FormStep` to split a form into multiple steps. Each step contains its own fields and can be validated independently.
451
+
452
+ ```python
453
+ from codeforms import Form, FormStep, TextField, EmailField, CheckboxField
454
+
455
+ form = Form(
456
+ name="registration",
457
+ content=[
458
+ FormStep(
459
+ title="Personal Information",
460
+ description="Tell us about yourself",
461
+ content=[
462
+ TextField(name="name", label="Name", required=True),
463
+ EmailField(name="email", label="Email", required=True),
464
+ ],
465
+ ),
466
+ FormStep(
467
+ title="Confirmation",
468
+ content=[
469
+ CheckboxField(name="terms", label="I accept the terms", required=True),
470
+ ],
471
+ validation_mode="on_submit",
472
+ ),
473
+ ],
474
+ )
475
+ ```
476
+
477
+ ### Step Validation
478
+
479
+ ```python
480
+ # Validate a single step
481
+ result = form.validate_step(0, {"name": "John", "email": "john@example.com"})
482
+ print(result["success"]) # True
483
+
484
+ # Validate all steps at once
485
+ result = form.validate_all_steps({
486
+ "name": "John",
487
+ "email": "john@example.com",
488
+ "terms": True,
489
+ })
490
+ print(result["success"]) # True
491
+ ```
492
+
493
+ ### Wizard Helpers
494
+
495
+ ```python
496
+ steps = form.get_steps() # List[FormStep]
497
+ fields = form.fields # Flat list of all fields across all steps
498
+ ```
499
+
500
+ ### HTML Export
501
+
502
+ Wizard forms export with `data-wizard="true"` on the `<form>` tag. Each step renders as a `<section data-step="true">` (not `<fieldset>`), so you can wire up your own step navigation in JavaScript.
503
+
504
+ See [`examples/wizard_form.py`](examples/wizard_form.py) for a full working example.
505
+
249
506
  ## Custom Field Types
250
507
 
251
508
  You can create your own field types by subclassing `FormFieldBase` and registering them with `register_field_type()`. Custom fields integrate seamlessly with forms, JSON serialization, validation, and HTML export.
@@ -6,14 +6,14 @@ and HTML export.
6
6
  """
7
7
 
8
8
  from codeforms import (
9
- Form,
10
- TextField,
11
- EmailField,
12
- SelectField,
13
9
  CheckboxField,
14
10
  CheckboxGroupField,
11
+ EmailField,
12
+ Form,
15
13
  RadioField,
14
+ SelectField,
16
15
  SelectOption,
16
+ TextField,
17
17
  ValidationRule,
18
18
  validate_form_data,
19
19
  )
@@ -205,7 +205,9 @@ if __name__ == "__main__":
205
205
  ],
206
206
  )
207
207
 
208
- contact_form.set_default_values(data={"name": "John Doe", "email": "john@example.com"})
208
+ contact_form.set_default_values(
209
+ data={"name": "John Doe", "email": "john@example.com"}
210
+ )
209
211
 
210
212
  # Export as Bootstrap 4 HTML
211
213
  print(contact_form.export(output_format="html_bootstrap4", id="my_form"))
@@ -7,9 +7,9 @@ y la diferencia entre validación legacy y dinámica.
7
7
 
8
8
  from codeforms import (
9
9
  Form,
10
- TextField,
11
10
  SelectField,
12
11
  SelectOption,
12
+ TextField,
13
13
  VisibilityRule,
14
14
  validate_form_data,
15
15
  validate_form_data_dynamic,
@@ -79,7 +79,9 @@ if __name__ == "__main__":
79
79
  print(f"US data: success={result['success']}")
80
80
  if not result["success"]:
81
81
  print(f" Errors: {result['errors']}")
82
- print(" (Province and County are required but missing — legacy doesn't know they're hidden)")
82
+ print(
83
+ " (Province and County are required but missing — legacy doesn't know they're hidden)"
84
+ )
83
85
 
84
86
  # --- Dynamic validation: respects visible_when ---
85
87
  print("\n=== Dynamic validate_form_data_dynamic (respects visible_when) ===")
@@ -11,22 +11,23 @@ from typing import Optional
11
11
  from pydantic import field_validator
12
12
 
13
13
  from codeforms import (
14
+ EmailField,
15
+ FieldGroup,
14
16
  Form,
15
17
  FormFieldBase,
16
- FieldGroup,
17
18
  TextField,
18
- EmailField,
19
- register_field_type,
20
19
  get_registered_field_types,
20
+ register_field_type,
21
21
  )
22
22
 
23
-
24
23
  # ---------------------------------------------------------------------------
25
24
  # 1. Define custom field types
26
25
  # ---------------------------------------------------------------------------
27
26
 
27
+
28
28
  class PhoneField(FormFieldBase):
29
29
  """A phone number field with an optional country code."""
30
+
30
31
  field_type: str = "phone"
31
32
  country_code: str = "+1"
32
33
  placeholder: Optional[str] = "e.g. +1-555-0100"
@@ -34,6 +35,7 @@ class PhoneField(FormFieldBase):
34
35
 
35
36
  class RatingField(FormFieldBase):
36
37
  """A numeric rating field with configurable range."""
38
+
37
39
  field_type: str = "rating"
38
40
  min_rating: int = 1
39
41
  max_rating: int = 5
@@ -49,6 +51,7 @@ class RatingField(FormFieldBase):
49
51
 
50
52
  class ColorField(FormFieldBase):
51
53
  """A colour picker field."""
54
+
52
55
  field_type: str = "color"
53
56
  color_format: str = "hex" # hex | rgb | hsl
54
57
 
@@ -6,10 +6,10 @@ de otro campo (por ejemplo, país → ciudades).
6
6
  """
7
7
 
8
8
  from codeforms import (
9
+ DependentOptionsConfig,
9
10
  Form,
10
11
  SelectField,
11
12
  SelectOption,
12
- DependentOptionsConfig,
13
13
  )
14
14
 
15
15
 
@@ -62,6 +62,7 @@ if __name__ == "__main__":
62
62
 
63
63
  # La metadata de dependencia se serializa a JSON
64
64
  import json
65
+
65
66
  data = json.loads(form.model_dump_json(exclude_none=True))
66
67
  city_field = data["content"][1]
67
68
  print("City field dependent_options:")