feat(forms): component-owned field validation + shared validator set (PMS-516) #350

Merged
nrupard merged 1 commit from feat/PMS-516-form-validation-layer into main 2026-06-25 19:26:59 +02:00
Owner

What

Foundation for the PMS-515 form-validation epic. Gives the shared field components their own validation and adds a single shared validator module, so forms stop hand-rolling ~76 ad-hoc validation strings with divergent messages.

Changes

  • src/utils/validation.rs (new): a Rule enum (Required, MinLen/MaxLen, Email, Uuid, Number{min,max,max_decimals}, Custom) + validate(value, label, rules) -> Option<String> returning the first failing rule's label-parameterised message. Pure and unit-tested (13 cases). Ships the broadly-applicable rules; domain-specific ones (phone E.164, postal, ISO country, H:MM) move onto shared rules as forms adopt them in PMS-518, with Custom as the interim escape hatch.
  • components/form.rs: Input, Textarea, Select, and DateField (via Input) take a rules: Vec<Rule> prop and self-validate once the field is touched (on blur, then live on subsequent input so the message clears as the value is corrected), driving the existing inline error slot. An explicit error prop still overrides, so a server field_message(...) lands in the same slot.
  • Corrects the stale DateField doc that still claimed required sets the native HTML attribute (MAPPS-277 removed it).

Non-breaking / opt-in

rules defaults to empty, so every existing call site behaves exactly as before (empty rules -> no computed error -> the error prop alone drives the slot, identical to the old code) until a form opts in during PMS-518. PartialEq on Rule is hand-written (comparing Custom via std::ptr::fn_addr_eq) to satisfy -D unpredictable_function_pointer_comparisons while keeping the enum usable in Dioxus Props.

Scope boundary

This is the component layer + validator module. Submit-time enforcement (forcing untouched fields to validate, blocking submit, focusing the first invalid field) is the separate submit guard in PMS-517; migrating the ~30 forms and deleting the bespoke validators is PMS-518. Checkbox is deferred (boolean value, different rule shape, no form needs it yet).

Verification

just check parity via the rust-builder image: clippy --all-targets -- -D warnings, fmt --check, cargo test --lib validation (30 passed), cargo check --target wasm32-unknown-unknown - all green.

Acceptance criteria (PMS-516)

  • Components enforce required + typed rules themselves and surface in the inline slot (on blur; submit-time forcing is PMS-517).
  • Typed rules (email, uuid, number min/max, maxlength) evaluated by the component; pattern-style via Custom.
  • A shared validation module with one canonical message per rule (consuming forms migrate in PMS-518).
  • The error prop still overrides for server field errors.
  • form.rs docs corrected (incl. the stale DateField comment).
  • just check green.
## What Foundation for the PMS-515 form-validation epic. Gives the shared field components their own validation and adds a single shared validator module, so forms stop hand-rolling ~76 ad-hoc validation strings with divergent messages. ## Changes - **`src/utils/validation.rs` (new):** a `Rule` enum (`Required`, `MinLen`/`MaxLen`, `Email`, `Uuid`, `Number{min,max,max_decimals}`, `Custom`) + `validate(value, label, rules) -> Option<String>` returning the first failing rule's label-parameterised message. Pure and unit-tested (13 cases). Ships the broadly-applicable rules; domain-specific ones (phone E.164, postal, ISO country, H:MM) move onto shared rules as forms adopt them in PMS-518, with `Custom` as the interim escape hatch. - **`components/form.rs`:** `Input`, `Textarea`, `Select`, and `DateField` (via `Input`) take a `rules: Vec<Rule>` prop and self-validate once the field is touched (on blur, then live on subsequent input so the message clears as the value is corrected), driving the existing inline `error` slot. An explicit `error` prop still overrides, so a server `field_message(...)` lands in the same slot. - Corrects the stale `DateField` doc that still claimed `required` sets the native HTML attribute (MAPPS-277 removed it). ## Non-breaking / opt-in `rules` defaults to empty, so every existing call site behaves exactly as before (empty rules -> no computed error -> the `error` prop alone drives the slot, identical to the old code) until a form opts in during PMS-518. `PartialEq` on `Rule` is hand-written (comparing `Custom` via `std::ptr::fn_addr_eq`) to satisfy `-D unpredictable_function_pointer_comparisons` while keeping the enum usable in Dioxus `Props`. ## Scope boundary This is the component layer + validator module. Submit-time enforcement (forcing untouched fields to validate, blocking submit, focusing the first invalid field) is the separate submit guard in **PMS-517**; migrating the ~30 forms and deleting the bespoke validators is **PMS-518**. `Checkbox` is deferred (boolean value, different rule shape, no form needs it yet). ## Verification `just check` parity via the rust-builder image: `clippy --all-targets -- -D warnings`, `fmt --check`, `cargo test --lib validation` (30 passed), `cargo check --target wasm32-unknown-unknown` - all green. ## Acceptance criteria (PMS-516) - [x] Components enforce `required` + typed rules themselves and surface in the inline slot (on blur; submit-time forcing is PMS-517). - [x] Typed rules (email, uuid, number min/max, maxlength) evaluated by the component; `pattern`-style via `Custom`. - [x] A shared validation module with one canonical message per rule (consuming forms migrate in PMS-518). - [x] The `error` prop still overrides for server field errors. - [x] `form.rs` docs corrected (incl. the stale `DateField` comment). - [x] `just check` green.
feat(forms): component-owned field validation + shared validator set (PMS-516)
All checks were successful
Check / fmt + clippy + tests (pull_request) Successful in 12m19s
Create release / Create release from merged PR (pull_request) Has been skipped
55d7c5e0b0
Foundation for the PMS-515 epic. Adds src/utils/validation.rs: a Rule enum (Required, MinLen/MaxLen, Email, Uuid, Number{min,max,max_decimals}, and a Custom escape hatch) plus validate(value, label, rules) that returns the first failing rule's label-parameterised message, with unit tests. This is the single place to express the common field rules so forms stop hand-rolling ~76 ad-hoc strings with divergent messages.

Wires the rule into the shared field components (Input, Textarea, Select, and DateField via Input): each takes a rules: Vec<Rule> prop and validates its value once the field is touched (on blur, then live on subsequent input so the error clears as it is fixed), driving its existing inline error slot. An explicit error prop (e.g. a server field_message) still overrides the rule message, so server errors land in the same slot.

Non-breaking and opt-in: rules defaults to empty, so every existing call site behaves exactly as before (empty rules -> no computed error -> the error prop alone drives the slot, identical to the old code) until a form adopts rules during the PMS-518 migration. Submit-time enforcement (forcing untouched fields to validate, blocking submit, focusing the first invalid field) is the separate submit guard in PMS-517; this commit delivers the component layer and the validator module it calls.

Also corrects the stale DateField doc that still claimed required sets the native HTML attribute (MAPPS-277 removed it). Checkbox is intentionally left for later: its value is boolean, not a string, so a "must be checked" rule has a different shape and no form needs it yet.

just check parity (rust-builder image): clippy --all-targets -D warnings, fmt --check, cargo test --lib validation (30 passed), cargo check --target wasm32-unknown-unknown all green.

#PMS-516
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-25 19:22:28 +02:00
nrupard deleted branch feat/PMS-516-form-validation-layer 2026-06-25 19:26:59 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!350
No description provided.