Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI-270] Adds new option shouldValidateBranch to createValidatedForm for helping to create branching forms. #442

Merged
merged 3 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .changeset/breezy-paws-hide.md
Original file line number Diff line number Diff line change
@@ -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<DinnerForm, DinnerForm>({
mealType: '',
pizzaToppings: createValidatedForm<DinnerForm['pizzaToppings'], DinnerForm>(
{
pepperoni: true,
pineapple: true,
},
{
shouldValidateBranch: (root) => root.mealType === 'pizza',
},
),
});
```
1 change: 1 addition & 0 deletions packages/forms/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
ValidationMessages,
ValidatedForm,
ValidatedFormOptions,
ValidatedFormControlOptions,
Templated,
} from './types';
export { createValidatedForm } from './stores/createValidatedForm';
49 changes: 42 additions & 7 deletions packages/forms/src/stores/createValidatedForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,9 +38,10 @@ import { isValidatedForm } from '../utils/isValidatedForm';
* - Another ValidatedForm store
* - A ValidatedFormArray store, to back an array.
*/
export const createValidatedForm = <T extends object>(
options: ValidatedFormOptions<T>,
): ValidatedForm<T> => {
export const createValidatedForm = <T extends object, Root = any>(
options: ValidatedFormOptions<T, Root>,
controlOptions: ValidatedFormControlOptions<T, Root> = {},
): ValidatedForm<T, Root> => {
const fullOptions = expandOptions(options);
const {
dataStore: { state: data, reset: resetData, onChange: onDataChange },
Expand Down Expand Up @@ -246,7 +254,11 @@ export const createValidatedForm = <T extends object>(
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<void>[] = [];
const toValidate = validationSets[trigger];
const triggers = includeTriggers(trigger);
Expand All @@ -255,11 +267,18 @@ export const createValidatedForm = <T extends object>(
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);
Expand Down Expand Up @@ -374,13 +393,21 @@ export const createValidatedForm = <T extends object>(
return false;
};

const validate = (event: ValidateOn, forceFocus = true) => {
const validate = (
event: ValidateOn,
forceFocus = true,
rootData?: Root,
) => {
return new Promise<boolean>((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;
Expand Down Expand Up @@ -599,5 +626,13 @@ export const createValidatedForm = <T extends object>(
[triggerValidation]: validate,
[focusError]: focusFirstError,
[insertError]: insertValidationError,
[shouldValidateBranch]: (root, trigger) => {
if (controlOptions.shouldValidateBranch == null) return true;
return controlOptions.shouldValidateBranch(
root,
fullData(),
trigger,
);
},
};
};
1 change: 1 addition & 0 deletions packages/forms/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
34 changes: 24 additions & 10 deletions packages/forms/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<T extends object, K extends keyof T> {
Expand Down Expand Up @@ -38,7 +39,7 @@ interface SetOptions {
templated?: Templated;
}

export interface ValidatedForm<T extends object> {
export interface ValidatedForm<T extends object, Root = any> {
/** The access the contained data directly */
readonly data: T;
/** If modifications are currently frozen, (for example, by submitting the data). */
Expand Down Expand Up @@ -87,7 +88,8 @@ export interface ValidatedForm<T extends object> {
/** @internal */
[triggerValidation]: (
trigger: ValidateOn,
forceFocus?: boolean,
forceFocus: boolean,
rootData: Root,
) => Promise<any>;
/** @internal */
[focusError]: () => Promise<boolean>;
Expand All @@ -98,6 +100,8 @@ export interface ValidatedForm<T extends object> {
message: string,
id: string,
) => void;
/** @internal */
[shouldValidateBranch]: (root: Root, trigger: ValidateOn) => boolean;
}

interface BasicConnection<K extends string, V> {
Expand Down Expand Up @@ -204,15 +208,25 @@ export type ExtendOptions<T> = {
* - Another ValidatedForm store
* - A ValidatedFormArray store, to back an array.
*/
export type ValidatedFormOptions<T> = {
export type ValidatedFormOptions<T extends object, Root = any> = {
[key in keyof T]: T[key] extends Array<any> | Map<any, any> | Set<any>
? FieldOptions<T[key], T> | T[key]
: T[key] extends object
? FieldOptions<T[key], T> | T[key] | ValidatedForm<T[key]>
? FieldOptions<T[key], T> | T[key] | ValidatedForm<T[key], Root>
: FieldOptions<T[key], T> | T[key];
};

/** Validation and setup otions for fields */
/** Additional global options for the entire validated form. */
export interface ValidatedFormControlOptions<T, Root = any> {
/** 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<ItemType, T> {
/** The initial value of the field. */
initialValue: ItemType;
Expand Down
2 changes: 1 addition & 1 deletion packages/forms/src/utils/expandOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const defaults: InternalFieldOptions<any, any> = {
templated: false,
};

export const expandOptions = <T>(
export const expandOptions = <T extends object>(
options: ValidatedFormOptions<T>,
): InternalValidatedFormOptions<T> => {
const expandedOptions: Record<
Expand Down