Skip to content

Form refactoring#333

Open
moust wants to merge 11 commits intomasterfrom
form-refactoring
Open

Form refactoring#333
moust wants to merge 11 commits intomasterfrom
form-refactoring

Conversation

@moust
Copy link
Copy Markdown
Contributor

@moust moust commented Mar 18, 2026

Replace the old custom form system with react-hook-form + Shadcn UI components.


1. Form Engine (src/lib/form.tsx, src/components/form/form.tsx)

  • New central form abstraction built on react-hook-form
  • Replaces formComponent.tsx, formControlsComponent.tsx, formFieldFactory.ts (all deleted)
  • Removes the custom src/core/validation.ts validator

2. Field Components (src/components/form/fields/)

  • Each old *Field.tsx (checkbox, date, identifier, password, phone, radiobox, select) was rewritten as a cleaner component
  • Phone number split into its own sub-module (phone/) with context, country variant, and non-country variant
  • New required.tsx and input.tsx primitives added

3. Shadcn UI Primitives (src/components/ui/)

  • Added: checkbox, dropdown-menu, field, input-group, item, label, progress, radio-group, select, separator, textarea
  • These are the low-level design system building blocks consumed by the field components

4. Form Fields Renderer (src/components/form/FormFieldsRenderer.tsx)

  • New generic component that maps field definitions to rendered form fields, replacing fieldCreator.tsx and formFieldFactory.ts

5. Widget Updates (src/widgets/*/)

  • All widgets migrated to the new form API: auth views (login, signup, forgot password, webauthn), MFA, profile editor, password editor, phone number editor, email editor, social accounts, step-up, passwordless, account recovery

6. Build / Infra

  • rollup.config.js updated (likely for new entry points or externals)
  • tailwind.config.cjs extended for Shadcn
  • components.json updated (Shadcn config)
  • CI (circleci/config.yml) switched to npm install for platform dependency compatibility

Diagram explaining the new form system architecture

flowchart TD                                                                                                                                         
      subgraph Widget["Widget (e.g. LoginWidget, SignupWidget)"]
          W[Widget Component]
      end                                                                                                                                              
                                                                                                                                                       
      subgraph FormLib["src/lib/form.tsx — Field Definition Layer"]
          FD["FieldDefinition\n(type, key, validation, transform, ...)"]
          PF["predefinedFields\n(email, password, phoneNumber, birthdate, consents, ...)"]
          GFD["getFieldDefinitions()\nResolves Field → FieldDefinition\n• predefined lookup\n• custom fields\n• consent fields"]
      end

      subgraph FormComp["src/components/form/form.tsx — Form Orchestrator"]
          FP["FormProvider\n(react-hook-form context)"]
          UF["useForm()\ndefaultValues, state, errors"]
          SUB["onSubmit\n• beforeSubmit transform\n• captchaHandler\n• handler(data)\n• handleSuccess / handleError"]
          ERR["Error handling\nsetError on root / field\ni18n error messages"]
      end

      subgraph FFR["src/components/form/FormFieldsRenderer.tsx — Field Router"]
          CTRL["Controller (per field)\n• rules.required\n• rules.validate → Zod schema"]
          RF["renderField()\nswitch on fieldDefinition.type"]
      end

      subgraph Fields["src/components/form/fields/ — Field Components"]
          IF["InputField\n(string, email, number, decimal, integer)"]
          PWF["PasswordField\n+ PasswordPolicyRules"]
          CHK["CheckboxField\n(consents, booleans)"]
          SEL["SelectField"]
          RG["RadioGroupField"]
          DF["DateField"]
          PHF["PhoneNumberField\n(international / domestic)"]
          IDN["IdentifierField\n(email or phone or both)"]
      end

      subgraph UI["src/components/ui/ — Shadcn UI Primitives"]
          UIB["Button, Input, Label\nCheckbox, Select, RadioGroup\nProgress, Textarea, Field, ..."]
      end
 
      subgraph Validation["Validation (per field)"]
          ZOD["Zod schema\nbuilt at validate-time\nwith: client, config, i18n, watch"]
          TRANS["transform.input / transform.output\nvalue coercion (e.g. UserConsent ↔ boolean)"]
      end                                                                                                                                              
                                                                                                                                                       
      W -->|"fields: Field[]"| GFD
      GFD -->|"FieldDefinition[]"| FP
      W -->|"handler, onSuccess, onError, initialModel"| FP

      FP --> UF
      UF -->|"control, formState, handleSubmit"| FFR
      UF --> SUB
         
      FFR --> CTRL
      CTRL -->|"Zod validate"| ZOD
      CTRL -->|"render"| RF
      RF --> IF & PWF & CHK & SEL & RG & DF & PHF & IDN

      IF & PWF & CHK & SEL & RG & DF & PHF & IDN --> UIB

      CTRL -->|"transform"| TRANS
      TRANS -->|"input: format for display"| RF
      TRANS -->|"output: format for RHF"| CTRL

      SUB -->|"formData"| W
Loading

How it flows:

  1. Widget declares a fields array (strings like "email" or full FieldDefinition objects) and a handler function.
  2. src/lib/form.tsxgetFieldDefinitions() resolves each field entry into a full FieldDefinition, looking up predefined fields, custom fields from config, or consent fields. Each definition carries a type, a Zod validation factory, and an optional transform (input/output coercion).
  3. Form component (src/components/form/form.tsx) calls useForm() from react-hook-form, wraps everything in FormProvider, and wires onSubmitcaptchaHandler → widget handler → success/error callbacks.
  4. FormFieldsRenderer iterates FieldDefinition[], wrapping each in a react-hook-form Controller. The Controller runs the Zod schema at validation time (with live access to client, config, i18n, and watch), then calls renderField() which dispatches on type to the right field component.
  5. Field components (fields/) are pure presentational components that consume Shadcn UI primitives. They receive onChange/value (already coerced via transform) and errors from the Controller.

@moust moust self-assigned this Mar 18, 2026
@moust moust force-pushed the form-refactoring branch 2 times, most recently from 4ecf05b to c213619 Compare March 18, 2026 14:11
@moust moust force-pushed the form-refactoring branch from c213619 to c81b8ed Compare March 18, 2026 14:19
- run:
name: Install dependencies
command: npm ci
command: npm install
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NPM 10 trims package-lock.json to only include optional native packages matching the current platform, whereas NPM 9 and earlier kept all platforms.
This caused npm ci to fail on linux because the lock file, generated on macOS, was missing the linux entries.

@moust moust marked this pull request as ready for review March 24, 2026 11:03
Copy link
Copy Markdown
Contributor

@thomasreach5 thomasreach5 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je ne suis pas en mesure de juger du contenu de la PR, en revanche on est d'accord que:

  • il n'y a strictement aucun changement d'API visible par nos clients ?
  • il n'y aura aucun problème de dépendances pour lui (s'il utilise déjà la lib sur laquelle on s'appuie maintenant.)
  • si on utilisait correctement le semver, cette évolution ne nécessiterait qu'un changement de version mineure voire patch.

@moust
Copy link
Copy Markdown
Contributor Author

moust commented Mar 25, 2026

Je ne suis pas en mesure de juger du contenu de la PR, en revanche on est d'accord que:

  • il n'y a strictement aucun changement d'API visible par nos clients ?
  • il n'y aura aucun problème de dépendances pour lui (s'il utilise déjà la lib sur laquelle on s'appuie maintenant.)
  • si on utilisait correctement le semver, cette évolution ne nécessiterait qu'un changement de version mineure voire patch.

Oui tout à fait. J'ai fait en sorte que les changements soient totalement transparents pour l'utilisateur et l'intégrateur.
À part quelques changements très léger au niveau de l'UI liés à la migration sur des composants Shadcn (par exemple le sélecteur de country sur le champ phone number) mais en terme d'UX ça ne change rien.
J'ai aussi fait en sorte de toujours prendre en compte les options de theming sur SDK UI (même si je n'ai aucun idée de la mesure de l'utilisation des ses options par les clients).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants