diff --git a/.changeset/breezy-paws-hide.md b/.changeset/breezy-paws-hide.md new file mode 100644 index 00000000..29c75afe --- /dev/null +++ b/.changeset/breezy-paws-hide.md @@ -0,0 +1,33 @@ +--- +'@kurrent-ui/forms': minor +--- + +`createValidatedForm` has a new option `shouldValidateBranch` for helping to create branching forms. + +When a parent validated form is validated, it will only call validation on a nested validated form if `shouldValidateBranch` returns true (or is `undefined`). +`shouldValidateBranch` is passed the data of the top level form which had validation called on it, it's own data, and the reason for validating. + +Example usage + +```ts +interface DinnerForm { + mealType: string; + pizzaToppings: { + pepperoni: boolean; + pineapple: boolean; + }; +} + +const form = createValidatedForm({ + mealType: '', + pizzaToppings: createValidatedForm( + { + pepperoni: true, + pineapple: true, + }, + { + shouldValidateBranch: (root) => root.mealType === 'pizza', + }, + ), +}); +``` diff --git a/packages/forms/src/index.ts b/packages/forms/src/index.ts index 5605da8d..7e60ba70 100644 --- a/packages/forms/src/index.ts +++ b/packages/forms/src/index.ts @@ -6,6 +6,7 @@ export type { ValidationMessages, ValidatedForm, ValidatedFormOptions, + ValidatedFormControlOptions, Templated, } from './types'; export { createValidatedForm } from './stores/createValidatedForm'; diff --git a/packages/forms/src/stores/createValidatedForm.ts b/packages/forms/src/stores/createValidatedForm.ts index 4fbd03f4..5d78e258 100644 --- a/packages/forms/src/stores/createValidatedForm.ts +++ b/packages/forms/src/stores/createValidatedForm.ts @@ -12,8 +12,15 @@ import type { ValidationMessages, ValidateOn, ValidationMessage, + ValidatedFormControlOptions, } from '../types'; -import { focusError, insertError, triggerValidation, wDKey } from '../symbols'; +import { + shouldValidateBranch, + focusError, + insertError, + triggerValidation, + wDKey, +} from '../symbols'; import { logger } from '../utils/logger'; import { expandOptions } from '../utils/expandOptions'; import { @@ -31,9 +38,10 @@ import { isValidatedForm } from '../utils/isValidatedForm'; * - Another ValidatedForm store * - A ValidatedFormArray store, to back an array. */ -export const createValidatedForm = ( - options: ValidatedFormOptions, -): ValidatedForm => { +export const createValidatedForm = ( + options: ValidatedFormOptions, + controlOptions: ValidatedFormControlOptions = {}, +): ValidatedForm => { const fullOptions = expandOptions(options); const { dataStore: { state: data, reset: resetData, onChange: onDataChange }, @@ -246,7 +254,11 @@ export const createValidatedForm = ( return new Set(triggers.slice(triggers.indexOf(trigger))); }; - const runValidation = async (trigger: ValidateOn, forceFocus = true) => { + const runValidation = async ( + trigger: ValidateOn, + forceFocus = true, + rootData?: Root, + ) => { const validationPromises: Promise[] = []; const toValidate = validationSets[trigger]; const triggers = includeTriggers(trigger); @@ -255,11 +267,18 @@ export const createValidatedForm = ( try { for (const [key, field] of fields) { if (isValidatedForm(field)) { + const dataToPass = rootData ?? fullData(); + + if (!field[shouldValidateBranch](dataToPass, trigger)) { + continue; + } + validationPromises.push( (async () => { const success = await field[triggerValidation]( trigger, false, + dataToPass, ); if (success) return; failures.add(key); @@ -374,13 +393,21 @@ export const createValidatedForm = ( return false; }; - const validate = (event: ValidateOn, forceFocus = true) => { + const validate = ( + event: ValidateOn, + forceFocus = true, + rootData?: Root, + ) => { return new Promise((resolve) => { forcingFocus = forcingFocus || forceFocus; awaiters.push(resolve); clearTimeout(validationTimeout); validationTimeout = window.setTimeout(async () => { - const success = await runValidation(event, forcingFocus); + const success = await runValidation( + event, + forcingFocus, + rootData, + ); awaiters.forEach((resolve) => resolve(success)); awaiters = []; forcingFocus = false; @@ -599,5 +626,13 @@ export const createValidatedForm = ( [triggerValidation]: validate, [focusError]: focusFirstError, [insertError]: insertValidationError, + [shouldValidateBranch]: (root, trigger) => { + if (controlOptions.shouldValidateBranch == null) return true; + return controlOptions.shouldValidateBranch( + root, + fullData(), + trigger, + ); + }, }; }; diff --git a/packages/forms/src/symbols.ts b/packages/forms/src/symbols.ts index cee9c9be..837c27df 100644 --- a/packages/forms/src/symbols.ts +++ b/packages/forms/src/symbols.ts @@ -2,3 +2,4 @@ export const wDKey = Symbol('formdata'); export const triggerValidation = Symbol('triggerValidation'); export const focusError = Symbol('focusError'); export const insertError = Symbol('insertError'); +export const shouldValidateBranch = Symbol('shouldValidateBranch'); diff --git a/packages/forms/src/types.ts b/packages/forms/src/types.ts index 2fc048fe..a9addcc9 100644 --- a/packages/forms/src/types.ts +++ b/packages/forms/src/types.ts @@ -1,9 +1,10 @@ import type { h as jsxFactory, VNode } from '@stencil/core'; -import { - type wDKey, - type focusError, - type insertError, - type triggerValidation, +import type { + wDKey, + focusError, + insertError, + triggerValidation, + shouldValidateBranch, } from './symbols'; interface ChangeEventValue { @@ -38,7 +39,7 @@ interface SetOptions { templated?: Templated; } -export interface ValidatedForm { +export interface ValidatedForm { /** The access the contained data directly */ readonly data: T; /** If modifications are currently frozen, (for example, by submitting the data). */ @@ -87,7 +88,8 @@ export interface ValidatedForm { /** @internal */ [triggerValidation]: ( trigger: ValidateOn, - forceFocus?: boolean, + forceFocus: boolean, + rootData: Root, ) => Promise; /** @internal */ [focusError]: () => Promise; @@ -98,6 +100,8 @@ export interface ValidatedForm { message: string, id: string, ) => void; + /** @internal */ + [shouldValidateBranch]: (root: Root, trigger: ValidateOn) => boolean; } interface BasicConnection { @@ -204,15 +208,25 @@ export type ExtendOptions = { * - Another ValidatedForm store * - A ValidatedFormArray store, to back an array. */ -export type ValidatedFormOptions = { +export type ValidatedFormOptions = { [key in keyof T]: T[key] extends Array | Map | Set ? FieldOptions | T[key] : T[key] extends object - ? FieldOptions | T[key] | ValidatedForm + ? FieldOptions | T[key] | ValidatedForm : FieldOptions | T[key]; }; -/** Validation and setup otions for fields */ +/** Additional global options for the entire validated form. */ +export interface ValidatedFormControlOptions { + /** Called to see if the child should be validated when the parent form is validated or submitted. */ + shouldValidateBranch?: ( + root: Root, + self: T, + trigger: ValidateOn, + ) => boolean; +} + +/** Validation and setup options for fields */ export interface FieldOptions { /** The initial value of the field. */ initialValue: ItemType; diff --git a/packages/forms/src/utils/expandOptions.ts b/packages/forms/src/utils/expandOptions.ts index 9e4aeaca..28900d42 100644 --- a/packages/forms/src/utils/expandOptions.ts +++ b/packages/forms/src/utils/expandOptions.ts @@ -16,7 +16,7 @@ const defaults: InternalFieldOptions = { templated: false, }; -export const expandOptions = ( +export const expandOptions = ( options: ValidatedFormOptions, ): InternalValidatedFormOptions => { const expandedOptions: Record<