@zeno-lib/forms 0.6.0 → 0.7.0
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.
- package/dist/create-zeno-form.d.mts +19 -4
- package/dist/create-zeno-form.mjs +29 -6
- package/dist/form-element.d.mts +16 -3
- package/dist/form-element.mjs +38 -18
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +2 -1
- package/dist/lib/date-value.d.mts +24 -0
- package/dist/lib/date-value.mjs +37 -0
- package/dist/lib/default-props.d.mts +7 -0
- package/dist/lib/default-props.mjs +13 -0
- package/dist/lib/empty-value.mjs +25 -1
- package/dist/lib/form-disabled.d.mts +28 -0
- package/dist/lib/form-disabled.mjs +54 -0
- package/dist/lib/item-options.d.mts +45 -0
- package/dist/lib/item-options.mjs +70 -0
- package/dist/lib/schema-required.mjs +17 -0
- package/dist/lib/use-field-control.d.mts +7 -0
- package/dist/lib/use-field-control.mjs +4 -1
- package/package.json +1 -1
- package/src/create-form.tsx +34 -29
- package/src/create-zeno-form.tsx +124 -51
- package/src/date-picker-field.test.tsx +108 -0
- package/src/default-props.test-d.ts +66 -0
- package/src/default-props.test.tsx +165 -0
- package/src/fields/checkbox-field.tsx +5 -0
- package/src/fields/checkbox-group-field.tsx +6 -2
- package/src/fields/combobox-field.tsx +107 -40
- package/src/fields/date-picker-field.tsx +34 -11
- package/src/fields/field-shell.tsx +8 -1
- package/src/fields/input-field.tsx +2 -0
- package/src/fields/multi-select-field.tsx +100 -42
- package/src/fields/otp-field.tsx +8 -2
- package/src/fields/radio-group-field.tsx +9 -1
- package/src/fields/reset-button.tsx +4 -0
- package/src/fields/select-field.tsx +9 -1
- package/src/fields/slider-field.tsx +9 -1
- package/src/fields/submit-button.tsx +3 -1
- package/src/fields/switch-field.tsx +5 -0
- package/src/fields/textarea-field.tsx +2 -0
- package/src/form-dialog.test.tsx +102 -1
- package/src/form-dialog.tsx +143 -42
- package/src/form-disabled.test.tsx +329 -0
- package/src/form-element.tsx +64 -29
- package/src/grouped-options.test-d.ts +59 -0
- package/src/grouped-options.test.tsx +251 -0
- package/src/index.ts +1 -0
- package/src/lib/date-value.test.ts +14 -0
- package/src/lib/date-value.ts +70 -0
- package/src/lib/default-props.ts +19 -0
- package/src/lib/empty-value.test.ts +19 -0
- package/src/lib/empty-value.ts +45 -1
- package/src/lib/form-disabled.ts +95 -0
- package/src/lib/item-options.test.ts +11 -0
- package/src/lib/item-options.ts +165 -0
- package/src/lib/schema-required.test.ts +9 -0
- package/src/lib/schema-required.ts +33 -0
- package/src/lib/use-field-control.ts +11 -1
- package/src/with-form.test-d.ts +76 -0
- package/src/with-form.test.tsx +60 -0
|
@@ -14,6 +14,11 @@ type StandardSchema<T> = {
|
|
|
14
14
|
};
|
|
15
15
|
type PartialFormData<T> = T extends readonly unknown[] ? T : T extends Date | RegExp | ((...args: never[]) => unknown) ? T : T extends object ? { [K in keyof T]?: PartialFormData<T[K]>; } : T;
|
|
16
16
|
type ZenoFormExtras<TFormData> = {
|
|
17
|
+
/**
|
|
18
|
+
* Disable every field of the form, its submit button and `FormDialog`'s
|
|
19
|
+
* Save. A field's own `disabled` still applies.
|
|
20
|
+
*/
|
|
21
|
+
disabled?: boolean;
|
|
17
22
|
/**
|
|
18
23
|
* If `true`, shipped fields skip rendering their inline `<FieldError>`
|
|
19
24
|
* message. Fields still flip `data-invalid` / `aria-invalid`.
|
|
@@ -44,9 +49,11 @@ type SchemaPathExtras<TFormData> = {
|
|
|
44
49
|
type ManualPathExtras = {
|
|
45
50
|
schema?: never;
|
|
46
51
|
};
|
|
47
|
-
type
|
|
48
|
-
type
|
|
49
|
-
type
|
|
52
|
+
type SyncValidator<TFormData> = undefined | FormValidateOrFn<TFormData>;
|
|
53
|
+
type AsyncValidator<TFormData> = undefined | FormAsyncValidateOrFn<TFormData>;
|
|
54
|
+
type SchemaFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues" | "validators" | "validationLogic"> & SchemaPathExtras<TFormData> & ZenoFormExtras<TFormData>;
|
|
55
|
+
type ManualFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = Omit<FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>, "defaultValues"> & ManualPathExtras & ZenoFormExtras<TFormData>;
|
|
56
|
+
type UseFormOptions<TFormData, TOnMount extends SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData>, TSubmitMeta> = SchemaFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> | ManualFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>;
|
|
50
57
|
type FormDataOf<F> = F extends {
|
|
51
58
|
state: {
|
|
52
59
|
values: infer V;
|
|
@@ -93,9 +100,17 @@ type WithControlValue<P, V> = P extends {
|
|
|
93
100
|
type FieldWrappers<FC extends Record<string, AnyComponent>, T> = { [K in keyof FC]: <N extends DeepKeys<T>>(props: WithControlValue<ComponentProps<FC[K]>, DeepValue<T, N>> & {
|
|
94
101
|
name: N;
|
|
95
102
|
} & WithValidators) => ReactNode; };
|
|
103
|
+
type OptionalProps<P> = P extends unknown ? { [K in keyof P as Pick<P, K> extends Required<Pick<P, K>> ? never : K]?: P[K]; } : never;
|
|
104
|
+
type DefaultProps<C extends Record<string, AnyComponent>> = { [K in keyof C]?: OptionalProps<ComponentProps<C[K]>>; };
|
|
96
105
|
type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>> = {
|
|
97
106
|
fieldComponents: FC;
|
|
98
107
|
formComponents: FMC;
|
|
108
|
+
/**
|
|
109
|
+
* Default props for field and form components, keyed by component name,
|
|
110
|
+
* e.g. `{ MoneyField: { currency: "CHF", locale: "de-CH" } }`. A prop
|
|
111
|
+
* passed at the call site wins; one passed as `undefined` keeps the default.
|
|
112
|
+
*/
|
|
113
|
+
defaultProps?: DefaultProps<NoInfer<FC> & NoInfer<FMC>>;
|
|
99
114
|
};
|
|
100
115
|
/**
|
|
101
116
|
* Build the wired Zeno form API from the caller's field + form components.
|
|
@@ -108,7 +123,7 @@ type CreateZenoFormConfig<FC extends Record<string, AnyComponent>, FMC extends R
|
|
|
108
123
|
export declare function createZenoForm<FC extends Record<string, AnyComponent>, FMC extends Record<string, AnyComponent>>(config: CreateZenoFormConfig<FC, FMC>): {
|
|
109
124
|
useAppFields: <TForm extends AnyAppForm>(form: TForm) => FieldWrappers<FC, FormDataOf<TForm>>;
|
|
110
125
|
useAppForm: <TFormData, TOnMount extends undefined | FormValidateOrFn<TFormData>, TOnChange extends undefined | FormValidateOrFn<TFormData>, TOnChangeAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnBlur extends undefined | FormValidateOrFn<TFormData>, TOnBlurAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnSubmit extends undefined | FormValidateOrFn<TFormData>, TOnSubmitAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnDynamic extends undefined | FormValidateOrFn<TFormData>, TOnDynamicAsync extends undefined | FormAsyncValidateOrFn<TFormData>, TOnServer extends undefined | FormAsyncValidateOrFn<TFormData>, TSubmitMeta>(props: FormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>;
|
|
111
|
-
useForm: <TFormData, TOnMount extends
|
|
126
|
+
useForm: <TFormData, TOnMount extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChange extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnChangeAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnBlur extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnBlurAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnSubmit extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnSubmitAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnDynamic extends SyncValidator<TFormData> = SyncValidator<TFormData>, TOnDynamicAsync extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TOnServer extends AsyncValidator<TFormData> = AsyncValidator<TFormData>, TSubmitMeta = unknown>(options: UseFormOptions<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta>) => import("@tanstack/react-form").FormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & import("@tanstack/react-form").ReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta> & NoInfer<FMC> & {
|
|
112
127
|
AppField: import("@tanstack/react-form").FieldComponent<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, NoInfer<FC>>;
|
|
113
128
|
AppForm: import("react").ComponentType<import("react").PropsWithChildren<{}>>;
|
|
114
129
|
} & FieldWrappers<FC, FormDataOf<import("@tanstack/react-form").AppFieldExtendedReactFormApi<TFormData, TOnMount, TOnChange, TOnChangeAsync, TOnBlur, TOnBlurAsync, TOnSubmit, TOnSubmitAsync, TOnDynamic, TOnDynamicAsync, TOnServer, TSubmitMeta, FC, FMC>>> & FMC;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { applyValidationError } from "./lib/apply-validation-error.mjs";
|
|
3
3
|
import { fieldContext, formContext } from "./lib/contexts.mjs";
|
|
4
|
+
import { withDefaultProps } from "./lib/default-props.mjs";
|
|
4
5
|
import { deepMergeDefaults, extractZodDefaults } from "./lib/schema-defaults.mjs";
|
|
5
6
|
import { getRequiredPaths } from "./lib/schema-required.mjs";
|
|
6
7
|
import { DEFAULT_VALIDATION_MODE, setFormZenoState } from "./lib/validation-modes.mjs";
|
|
7
8
|
import { withEmptyStartValues } from "./lib/empty-value.mjs";
|
|
9
|
+
import { useDisabledOption } from "./lib/form-disabled.mjs";
|
|
8
10
|
import { setSubmitError } from "./lib/submit-error.mjs";
|
|
9
11
|
import { useRebasedDefaultValues } from "./lib/use-rebased-default-values.mjs";
|
|
10
12
|
import { useUnsavedChangesWarning } from "./lib/use-unsaved-changes-warning.mjs";
|
|
@@ -31,6 +33,24 @@ function withMountListener(listeners, onMount) {
|
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
35
|
}
|
|
36
|
+
const REACT_TYPE_KEYS = /* @__PURE__ */ new Set([
|
|
37
|
+
"$$typeof",
|
|
38
|
+
"compare",
|
|
39
|
+
"render",
|
|
40
|
+
"type"
|
|
41
|
+
]);
|
|
42
|
+
function withComponentDefaults(components, defaultProps) {
|
|
43
|
+
const wrapped = { ...components };
|
|
44
|
+
for (const [name, defaults] of Object.entries(defaultProps ?? {})) {
|
|
45
|
+
const Component = components[name];
|
|
46
|
+
if (Component === void 0 || defaults === void 0) continue;
|
|
47
|
+
const Defaulted = (props) => /* @__PURE__ */ jsx(Component, { ...withDefaultProps(defaults, props) });
|
|
48
|
+
for (const [key, value] of Object.entries(Component)) if (!REACT_TYPE_KEYS.has(key)) Object.assign(Defaulted, { [key]: value });
|
|
49
|
+
Defaulted.displayName = name;
|
|
50
|
+
wrapped[name] = Defaulted;
|
|
51
|
+
}
|
|
52
|
+
return wrapped;
|
|
53
|
+
}
|
|
34
54
|
/**
|
|
35
55
|
* Build the wired Zeno form API from the caller's field + form components.
|
|
36
56
|
*
|
|
@@ -40,18 +60,20 @@ function withMountListener(listeners, onMount) {
|
|
|
40
60
|
* `useAppFields` wrappers (prop types are inferred from the injected components).
|
|
41
61
|
*/
|
|
42
62
|
function createZenoForm(config) {
|
|
63
|
+
const fieldComponents = withComponentDefaults(config.fieldComponents, config.defaultProps);
|
|
64
|
+
const formComponents = withComponentDefaults(config.formComponents, config.defaultProps);
|
|
43
65
|
const { useAppForm, withFieldGroup, withForm } = createFormHook({
|
|
44
|
-
fieldComponents
|
|
66
|
+
fieldComponents,
|
|
45
67
|
fieldContext,
|
|
46
|
-
formComponents
|
|
68
|
+
formComponents,
|
|
47
69
|
formContext
|
|
48
70
|
});
|
|
49
71
|
function useAppFields(form) {
|
|
50
72
|
return useMemo(() => {
|
|
51
73
|
const Field = form.AppField;
|
|
52
74
|
const wrappers = {};
|
|
53
|
-
for (const key of Object.keys(
|
|
54
|
-
const Impl =
|
|
75
|
+
for (const key of Object.keys(fieldComponents)) {
|
|
76
|
+
const Impl = fieldComponents[key];
|
|
55
77
|
wrappers[key] = ({ name, validators, listeners, ...props }) => /* @__PURE__ */ jsx(Field, {
|
|
56
78
|
listeners,
|
|
57
79
|
name,
|
|
@@ -63,7 +85,7 @@ function createZenoForm(config) {
|
|
|
63
85
|
}, [form]);
|
|
64
86
|
}
|
|
65
87
|
function useForm(options) {
|
|
66
|
-
const { schema, validators: validatorsInput, validationLogic: userValidationLogic, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
|
|
88
|
+
const { schema, validators: validatorsInput, validationLogic: userValidationLogic, disabled = false, hideFieldErrors = false, requiredIndicator = true, unsavedChangesWarning = false, defaultValues: userDefaultValues, onSubmit: userOnSubmit, ...rest } = options;
|
|
67
89
|
let schemaMode;
|
|
68
90
|
let resolvedValidators;
|
|
69
91
|
let resolvedValidationLogic;
|
|
@@ -111,9 +133,10 @@ function createZenoForm(config) {
|
|
|
111
133
|
schema,
|
|
112
134
|
...schemaMode === void 0 ? {} : { validation: schemaMode }
|
|
113
135
|
});
|
|
136
|
+
useDisabledOption(form, disabled);
|
|
114
137
|
useUnsavedChangesWarning(form, unsavedChangesWarning);
|
|
115
138
|
const fields = useAppFields(form);
|
|
116
|
-
return useMemo(() => Object.assign(form, fields,
|
|
139
|
+
return useMemo(() => Object.assign(form, fields, formComponents), [form, fields]);
|
|
117
140
|
}
|
|
118
141
|
return {
|
|
119
142
|
useAppFields,
|
package/dist/form-element.d.mts
CHANGED
|
@@ -2,12 +2,25 @@ import { ComponentProps, ReactNode } from "react";
|
|
|
2
2
|
//#region src/form-element.d.ts
|
|
3
3
|
type FormProviderProps = {
|
|
4
4
|
children: ReactNode;
|
|
5
|
+
/**
|
|
6
|
+
* Disable every field of the form and its submit button, as
|
|
7
|
+
* `useForm({ disabled })` does. A field's own `disabled` still applies.
|
|
8
|
+
*/
|
|
9
|
+
disabled?: boolean;
|
|
5
10
|
form: {
|
|
6
11
|
handleSubmit: () => unknown;
|
|
7
12
|
};
|
|
8
13
|
};
|
|
9
|
-
declare function FormProvider({ children, form }: FormProviderProps): import("react").JSX.Element;
|
|
10
|
-
type FormProps = Omit<ComponentProps<"form">, "onSubmit"
|
|
11
|
-
|
|
14
|
+
declare function FormProvider({ children, disabled, form }: FormProviderProps): import("react").JSX.Element;
|
|
15
|
+
type FormProps = Omit<ComponentProps<"form">, "onSubmit"> & {
|
|
16
|
+
/**
|
|
17
|
+
* Disable every field and button of the kit inside, and ignore a submit. A
|
|
18
|
+
* field's own `disabled` still applies. A button outside the `<form>`
|
|
19
|
+
* (`form="id"`) isn't inside: disable the form with `useForm({ disabled })`
|
|
20
|
+
* or `<FormProvider disabled>` to reach it.
|
|
21
|
+
*/
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
};
|
|
24
|
+
declare function Form({ children, className, disabled, ...props }: FormProps): import("react").JSX.Element;
|
|
12
25
|
//#endregion
|
|
13
26
|
export { Form, FormProvider };
|
package/dist/form-element.mjs
CHANGED
|
@@ -1,32 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { FormProvider as FormProvider$1, useFormContext } from "./lib/contexts.mjs";
|
|
3
|
+
import { FormDisabledContext, useIsFormDisabled } from "./lib/form-disabled.mjs";
|
|
3
4
|
import { reportSubmitError } from "./lib/submit-error.mjs";
|
|
4
5
|
import { focusFirstInvalid, restoreFocus } from "./lib/focus-first-invalid.mjs";
|
|
6
|
+
import { useMemo } from "react";
|
|
5
7
|
import { jsx } from "react/jsx-runtime";
|
|
6
8
|
//#region src/form-element.tsx
|
|
7
|
-
function FormProvider({ children, form }) {
|
|
9
|
+
function FormProvider({ children, disabled = false, form }) {
|
|
10
|
+
const { store } = form;
|
|
11
|
+
const scope = useMemo(() => ({
|
|
12
|
+
disabled,
|
|
13
|
+
key: store
|
|
14
|
+
}), [disabled, store]);
|
|
8
15
|
return /* @__PURE__ */ jsx(FormProvider$1, {
|
|
9
16
|
value: form,
|
|
10
|
-
children
|
|
17
|
+
children: /* @__PURE__ */ jsx(FormDisabledContext, {
|
|
18
|
+
value: scope,
|
|
19
|
+
children
|
|
20
|
+
})
|
|
11
21
|
});
|
|
12
22
|
}
|
|
13
|
-
function Form({ children, className, ...props }) {
|
|
23
|
+
function Form({ children, className, disabled = false, ...props }) {
|
|
14
24
|
const form = useFormContext();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const isDisabled = useIsFormDisabled(form) || disabled;
|
|
26
|
+
const { store } = form;
|
|
27
|
+
const scope = useMemo(() => ({
|
|
28
|
+
disabled: isDisabled,
|
|
29
|
+
key: store
|
|
30
|
+
}), [isDisabled, store]);
|
|
31
|
+
return /* @__PURE__ */ jsx(FormDisabledContext, {
|
|
32
|
+
value: scope,
|
|
33
|
+
children: /* @__PURE__ */ jsx("form", {
|
|
34
|
+
className,
|
|
35
|
+
noValidate: true,
|
|
36
|
+
onSubmit: async (event) => {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
event.stopPropagation();
|
|
39
|
+
if (isDisabled) return;
|
|
40
|
+
const node = event.currentTarget;
|
|
41
|
+
const focused = node.ownerDocument.activeElement;
|
|
42
|
+
await Promise.resolve(form.handleSubmit()).catch(reportSubmitError);
|
|
43
|
+
const { isSubmitSuccessful, isValid } = form.state;
|
|
44
|
+
if (!isValid && focusFirstInvalid(node)) return;
|
|
45
|
+
if (!(isValid && isSubmitSuccessful)) restoreFocus(focused);
|
|
46
|
+
},
|
|
47
|
+
...props,
|
|
48
|
+
children
|
|
49
|
+
})
|
|
30
50
|
});
|
|
31
51
|
}
|
|
32
52
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -6,8 +6,9 @@ import { ActionError, ActionIssue, ActionResult, toActionError, toFieldName } fr
|
|
|
6
6
|
import { FieldMessage, ValidationError } from "./lib/validation-error.mjs";
|
|
7
7
|
import { applyValidationError } from "./lib/apply-validation-error.mjs";
|
|
8
8
|
import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
|
|
9
|
+
import { useIsFormDisabled } from "./lib/form-disabled.mjs";
|
|
9
10
|
import { SubmitActionOptions, SubmitActionSchema, applyActionError, submitAction } from "./lib/submit-action.mjs";
|
|
10
11
|
import { useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid } from "./lib/use-is-invalid.mjs";
|
|
11
12
|
import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
|
|
12
13
|
import { useSelector } from "@tanstack/react-form";
|
|
13
|
-
export { type ActionError, type ActionIssue, type ActionResult, type FieldControl, type FieldControlProps, type FieldMessage, Form, FormProvider, type SubmitActionOptions, type SubmitActionSchema, type UseFieldControlOptions, ValidationError, type ValidationMode, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid, useSelector };
|
|
14
|
+
export { type ActionError, type ActionIssue, type ActionResult, type FieldControl, type FieldControlProps, type FieldMessage, Form, FormProvider, type SubmitActionOptions, type SubmitActionSchema, type UseFieldControlOptions, ValidationError, type ValidationMode, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsFormDisabled, useIsInvalid, useSelector };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { applyValidationError } from "./lib/apply-validation-error.mjs";
|
|
2
2
|
import { useFieldContext, useFormContext } from "./lib/contexts.mjs";
|
|
3
3
|
import { toActionError, toFieldName } from "./lib/action-result.mjs";
|
|
4
|
+
import { useIsFormDisabled } from "./lib/form-disabled.mjs";
|
|
4
5
|
import { ValidationError } from "./lib/validation-error.mjs";
|
|
5
6
|
import { blurThenChangeLogic } from "./lib/validation-logic.mjs";
|
|
6
7
|
import { createZenoForm } from "./create-zeno-form.mjs";
|
|
@@ -9,4 +10,4 @@ import { applyActionError, submitAction } from "./lib/submit-action.mjs";
|
|
|
9
10
|
import { useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid } from "./lib/use-is-invalid.mjs";
|
|
10
11
|
import { useFieldControl } from "./lib/use-field-control.mjs";
|
|
11
12
|
import { useSelector } from "@tanstack/react-form";
|
|
12
|
-
export { Form, FormProvider, ValidationError, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsInvalid, useSelector };
|
|
13
|
+
export { Form, FormProvider, ValidationError, applyActionError, applyValidationError, blurThenChangeLogic, createZenoForm, submitAction, toActionError, toFieldName, useFieldContext, useFieldControl, useFormContext, useHideFieldErrors, useIsFieldRequired, useIsFieldRequiredBySchema, useIsFormDisabled, useIsInvalid, useSelector };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/lib/date-value.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* What a date field stores for a picked day:
|
|
4
|
+
* - `"date"`: a `Date`, at local midnight.
|
|
5
|
+
* - `"iso-date"`: the day as `YYYY-MM-DD`, as `z.iso.date()` accepts and a
|
|
6
|
+
* `date` column stores it.
|
|
7
|
+
* - `"iso-datetime"`: the day's local midnight as an ISO 8601 timestamp in
|
|
8
|
+
* UTC (`toISOString()`), as `z.iso.datetime()` accepts and a timestamp
|
|
9
|
+
* column takes it.
|
|
10
|
+
*/
|
|
11
|
+
type DateValueType = "date" | "iso-date" | "iso-datetime";
|
|
12
|
+
/**
|
|
13
|
+
* The day a stored value stands for, as a local `Date`, or `undefined` for an
|
|
14
|
+
* empty or unreadable one. Reads a `Date`, a `YYYY-MM-DD` string as that day
|
|
15
|
+
* in local time, and any other string `Date` parses, such as an ISO 8601
|
|
16
|
+
* timestamp.
|
|
17
|
+
*/
|
|
18
|
+
declare function parseDateValue(value: unknown): Date | undefined;
|
|
19
|
+
/** The day of a local `Date` as `YYYY-MM-DD`, without going through UTC. */
|
|
20
|
+
declare function formatIsoDate(date: Date): string;
|
|
21
|
+
/** The value to store for `date`, a day picked in local time. */
|
|
22
|
+
declare function toDateValue(date: Date, type: DateValueType): Date | string;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { type DateValueType, formatIsoDate, parseDateValue, toDateValue };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/lib/date-value.ts
|
|
2
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
3
|
+
function pad(value, length) {
|
|
4
|
+
return String(value).padStart(length, "0");
|
|
5
|
+
}
|
|
6
|
+
function parseIsoDate(value) {
|
|
7
|
+
const match = ISO_DATE.exec(value);
|
|
8
|
+
if (!match) return;
|
|
9
|
+
const [year, month, day] = match.slice(1).map(Number);
|
|
10
|
+
const date = new Date(year, month - 1, day);
|
|
11
|
+
date.setFullYear(year, month - 1, day);
|
|
12
|
+
return date.getMonth() === month - 1 ? date : void 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The day a stored value stands for, as a local `Date`, or `undefined` for an
|
|
16
|
+
* empty or unreadable one. Reads a `Date`, a `YYYY-MM-DD` string as that day
|
|
17
|
+
* in local time, and any other string `Date` parses, such as an ISO 8601
|
|
18
|
+
* timestamp.
|
|
19
|
+
*/
|
|
20
|
+
function parseDateValue(value) {
|
|
21
|
+
let date;
|
|
22
|
+
if (value instanceof Date) date = value;
|
|
23
|
+
else if (typeof value === "string" && value !== "") date = ISO_DATE.test(value) ? parseIsoDate(value) : new Date(value);
|
|
24
|
+
return date && !Number.isNaN(date.getTime()) ? date : void 0;
|
|
25
|
+
}
|
|
26
|
+
/** The day of a local `Date` as `YYYY-MM-DD`, without going through UTC. */
|
|
27
|
+
function formatIsoDate(date) {
|
|
28
|
+
return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1, 2)}-${pad(date.getDate(), 2)}`;
|
|
29
|
+
}
|
|
30
|
+
/** The value to store for `date`, a day picked in local time. */
|
|
31
|
+
function toDateValue(date, type) {
|
|
32
|
+
if (type === "iso-date") return formatIsoDate(date);
|
|
33
|
+
if (type === "iso-datetime") return date.toISOString();
|
|
34
|
+
return date;
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { formatIsoDate, parseDateValue, toDateValue };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/lib/default-props.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The props a component renders with: `props` over `defaults`. A prop passed
|
|
4
|
+
* as `undefined` keeps its default, as a destructuring default would.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withDefaultProps<P extends object>(defaults: Partial<P> | undefined, props: P): P;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/lib/default-props.ts
|
|
2
|
+
/**
|
|
3
|
+
* The props a component renders with: `props` over `defaults`. A prop passed
|
|
4
|
+
* as `undefined` keeps its default, as a destructuring default would.
|
|
5
|
+
*/
|
|
6
|
+
function withDefaultProps(defaults, props) {
|
|
7
|
+
if (defaults === void 0) return props;
|
|
8
|
+
const merged = { ...defaults };
|
|
9
|
+
for (const [key, value] of Object.entries(props)) if (value !== void 0) merged[key] = value;
|
|
10
|
+
return merged;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { withDefaultProps };
|
package/dist/lib/empty-value.mjs
CHANGED
|
@@ -44,14 +44,38 @@ function emptyValueFor(schema, values, name, fallback) {
|
|
|
44
44
|
function isTypeEmpty(value) {
|
|
45
45
|
return value === "" || Array.isArray(value) && value.length === 0;
|
|
46
46
|
}
|
|
47
|
+
function isUnder(name, prefix) {
|
|
48
|
+
return name === prefix || name.startsWith(`${prefix}.`) || name.startsWith(`${prefix}[`);
|
|
49
|
+
}
|
|
50
|
+
function emptiableAncestor(schema, values, keys) {
|
|
51
|
+
for (let length = 1; length < keys.length; length += 1) {
|
|
52
|
+
const prefix = keys.slice(0, length);
|
|
53
|
+
const start = valueAt(values, prefix);
|
|
54
|
+
if (typeof start !== "object" || start === null || Array.isArray(start)) continue;
|
|
55
|
+
const empty = emptyValueFor(schema, values, toFieldName(prefix), start);
|
|
56
|
+
if (empty === null || empty === void 0) return {
|
|
57
|
+
keys: prefix,
|
|
58
|
+
value: empty
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
47
63
|
function withEmptyStartValues(schema, defaults) {
|
|
48
64
|
const issues = validateSync(schema, defaults);
|
|
49
65
|
let result = defaults;
|
|
66
|
+
const emptied = [];
|
|
50
67
|
for (const issue of issues ?? []) {
|
|
51
68
|
const name = toFieldName(issue.path);
|
|
52
69
|
const keys = toPathKeys(name);
|
|
70
|
+
if (keys.length === 0 || emptied.some((prefix) => isUnder(name, prefix))) continue;
|
|
71
|
+
const ancestor = emptiableAncestor(schema, result, keys);
|
|
72
|
+
if (ancestor) {
|
|
73
|
+
result = withValueAt(result, ancestor.keys, ancestor.value);
|
|
74
|
+
emptied.push(toFieldName(ancestor.keys));
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
53
77
|
const start = valueAt(defaults, keys);
|
|
54
|
-
if (
|
|
78
|
+
if (!isTypeEmpty(start)) continue;
|
|
55
79
|
const empty = emptyValueFor(schema, result, name, start);
|
|
56
80
|
if (empty !== start) result = withValueAt(result, keys, empty);
|
|
57
81
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { AnyFormApi } from "@tanstack/react-form";
|
|
2
|
+
//#region src/lib/form-disabled.d.ts
|
|
3
|
+
/** Apply `useForm({ disabled })` to `form`. */
|
|
4
|
+
declare function useDisabledOption(form: AnyFormApi, disabled: boolean): void;
|
|
5
|
+
type FormDisabledScope = {
|
|
6
|
+
readonly disabled: boolean;
|
|
7
|
+
/** The `store` of the form it disables. */
|
|
8
|
+
readonly key: object;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* The disabled state `FormProvider` and `<Form>` give what they wrap, for
|
|
12
|
+
* their own form only: a field of another form rendered inside (a quick-add
|
|
13
|
+
* popover) keeps that form's state.
|
|
14
|
+
*/
|
|
15
|
+
declare const FormDisabledContext: import("react").Context<FormDisabledScope | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Whether the form is disabled, by `useForm({ disabled })`,
|
|
18
|
+
* `<FormProvider disabled>`, `<Form disabled>` or `FormDialog`'s `disabled`.
|
|
19
|
+
* Every kit field reads it, so a control the fieldset of a disabled dialog
|
|
20
|
+
* can't reach (a checkbox, a switch, a radio, a slider) turns off too.
|
|
21
|
+
*
|
|
22
|
+
* Reads the form of the nearest provider; inside a field, pass `field.form`.
|
|
23
|
+
* `useForm({ disabled })` applies even where no provider of the form is
|
|
24
|
+
* mounted.
|
|
25
|
+
*/
|
|
26
|
+
declare function useIsFormDisabled(form?: AnyFormApi): boolean;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { FormDisabledContext, useDisabledOption, useIsFormDisabled };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { formContext } from "./contexts.mjs";
|
|
3
|
+
import { createContext, useCallback, useContext, useLayoutEffect, useSyncExternalStore } from "react";
|
|
4
|
+
//#region src/lib/form-disabled.ts
|
|
5
|
+
const disabledOption = /* @__PURE__ */ new WeakMap();
|
|
6
|
+
const listeners = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function subscribe(key, listener) {
|
|
8
|
+
let set = listeners.get(key);
|
|
9
|
+
if (!set) {
|
|
10
|
+
set = /* @__PURE__ */ new Set();
|
|
11
|
+
listeners.set(key, set);
|
|
12
|
+
}
|
|
13
|
+
set.add(listener);
|
|
14
|
+
return () => {
|
|
15
|
+
set.delete(listener);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Apply `useForm({ disabled })` to `form`. */
|
|
19
|
+
function useDisabledOption(form, disabled) {
|
|
20
|
+
const key = form.store;
|
|
21
|
+
if (!disabledOption.has(key)) disabledOption.set(key, disabled);
|
|
22
|
+
useLayoutEffect(() => {
|
|
23
|
+
if (disabledOption.get(key) === disabled) return;
|
|
24
|
+
disabledOption.set(key, disabled);
|
|
25
|
+
for (const listener of listeners.get(key) ?? []) listener();
|
|
26
|
+
}, [key, disabled]);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The disabled state `FormProvider` and `<Form>` give what they wrap, for
|
|
30
|
+
* their own form only: a field of another form rendered inside (a quick-add
|
|
31
|
+
* popover) keeps that form's state.
|
|
32
|
+
*/
|
|
33
|
+
const FormDisabledContext = createContext(null);
|
|
34
|
+
/**
|
|
35
|
+
* Whether the form is disabled, by `useForm({ disabled })`,
|
|
36
|
+
* `<FormProvider disabled>`, `<Form disabled>` or `FormDialog`'s `disabled`.
|
|
37
|
+
* Every kit field reads it, so a control the fieldset of a disabled dialog
|
|
38
|
+
* can't reach (a checkbox, a switch, a radio, a slider) turns off too.
|
|
39
|
+
*
|
|
40
|
+
* Reads the form of the nearest provider; inside a field, pass `field.form`.
|
|
41
|
+
* `useForm({ disabled })` applies even where no provider of the form is
|
|
42
|
+
* mounted.
|
|
43
|
+
*/
|
|
44
|
+
function useIsFormDisabled(form) {
|
|
45
|
+
const scope = useContext(FormDisabledContext);
|
|
46
|
+
const contextForm = useContext(formContext);
|
|
47
|
+
const key = (form ?? contextForm)?.store;
|
|
48
|
+
const subscribeToKey = useCallback((listener) => key ? subscribe(key, listener) : () => null, [key]);
|
|
49
|
+
const getSnapshot = () => key ? disabledOption.get(key) ?? false : false;
|
|
50
|
+
const byOption = useSyncExternalStore(subscribeToKey, getSnapshot, getSnapshot);
|
|
51
|
+
return scope?.disabled === true && (key === void 0 || scope.key === key) || byOption;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { FormDisabledContext, useDisabledOption, useIsFormDisabled };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/lib/item-options.d.ts
|
|
2
|
+
/** An item whose value isn't its label. */
|
|
3
|
+
type ItemObject<V> = {
|
|
4
|
+
value: V;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
/** Items under a heading. */
|
|
8
|
+
type ItemGroup<T> = {
|
|
9
|
+
label: string;
|
|
10
|
+
items: readonly T[];
|
|
11
|
+
};
|
|
12
|
+
/** An item of `items`, or of one of its groups. */
|
|
13
|
+
type ItemOption<T> = T extends ItemGroup<infer I> ? I : T;
|
|
14
|
+
/** What picking an item stores: `item.value` for an object, else the item. */
|
|
15
|
+
type ItemValue<T> = T extends ItemGroup<infer I> ? ItemValue<I> : T extends ItemObject<infer V> ? V : T;
|
|
16
|
+
/** Whether an item's value is a stored one, as `isItemEqualToValue` says. */
|
|
17
|
+
type ValueComparator = (itemValue: unknown, value: unknown) => boolean;
|
|
18
|
+
declare function isItemObject(item: unknown): item is ItemObject<unknown>;
|
|
19
|
+
/** What Base UI takes for a group: an object with an `items` array. */
|
|
20
|
+
declare function isItemGroup(item: unknown): item is ItemGroup<unknown>;
|
|
21
|
+
declare function itemValue(item: unknown): unknown;
|
|
22
|
+
declare function itemLabel(item: unknown): string;
|
|
23
|
+
/** A React key per value: `1` and `"1"` get two, an object gets its JSON. */
|
|
24
|
+
declare function valueKey(value: unknown): string;
|
|
25
|
+
type ItemList<T> = {
|
|
26
|
+
/**
|
|
27
|
+
* `items` as Base UI takes them. It tells a grouped list by its first entry
|
|
28
|
+
* alone, so once there is a group, each run of plain items beside one goes
|
|
29
|
+
* under a group with no heading (`label: ""`).
|
|
30
|
+
*/
|
|
31
|
+
readonly items: readonly T[];
|
|
32
|
+
/** Every option, the grouped ones included, in order. */
|
|
33
|
+
readonly options: readonly ItemOption<T>[];
|
|
34
|
+
/** The first option of each `valueKey`, so finding a value is no scan. */
|
|
35
|
+
readonly byKey: ReadonlyMap<string, ItemOption<T>>;
|
|
36
|
+
};
|
|
37
|
+
declare function toItemList<T>(items: readonly T[]): ItemList<T>;
|
|
38
|
+
/**
|
|
39
|
+
* `Object.is`, then `isItemEqualToValue`, which never sees an empty value.
|
|
40
|
+
*/
|
|
41
|
+
declare function isSameValue(a: unknown, b: unknown, isItemEqualToValue: ValueComparator | undefined): boolean;
|
|
42
|
+
/** The option whose value is `value`, if `items` has it. */
|
|
43
|
+
declare function findOption<T>(list: ItemList<T>, value: unknown, isItemEqualToValue: ValueComparator | undefined): ItemOption<T> | undefined;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { type ItemGroup, type ItemList, type ItemObject, type ItemOption, type ItemValue, type ValueComparator, findOption, isItemGroup, isItemObject, isSameValue, itemLabel, itemValue, toItemList, valueKey };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/lib/item-options.ts
|
|
2
|
+
function isItemObject(item) {
|
|
3
|
+
return typeof item === "object" && item !== null && "value" in item && "label" in item;
|
|
4
|
+
}
|
|
5
|
+
/** What Base UI takes for a group: an object with an `items` array. */
|
|
6
|
+
function isItemGroup(item) {
|
|
7
|
+
return typeof item === "object" && item !== null && Array.isArray(item.items);
|
|
8
|
+
}
|
|
9
|
+
function itemValue(item) {
|
|
10
|
+
return isItemObject(item) ? item.value : item;
|
|
11
|
+
}
|
|
12
|
+
function itemLabel(item) {
|
|
13
|
+
return isItemObject(item) ? item.label : String(item);
|
|
14
|
+
}
|
|
15
|
+
/** A React key per value: `1` and `"1"` get two, an object gets its JSON. */
|
|
16
|
+
function valueKey(value) {
|
|
17
|
+
if (typeof value !== "object" || value === null) return `${typeof value}:${String(value)}`;
|
|
18
|
+
try {
|
|
19
|
+
return `object:${JSON.stringify(value)}`;
|
|
20
|
+
} catch {
|
|
21
|
+
return `object:${String(value)}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function toItemList(items) {
|
|
25
|
+
const hasGroup = items.some(isItemGroup);
|
|
26
|
+
const options = [];
|
|
27
|
+
const groups = [];
|
|
28
|
+
let run;
|
|
29
|
+
for (const item of items) if (isItemGroup(item)) {
|
|
30
|
+
for (const option of item.items) options.push(option);
|
|
31
|
+
groups.push(item);
|
|
32
|
+
run = void 0;
|
|
33
|
+
} else {
|
|
34
|
+
options.push(item);
|
|
35
|
+
if (hasGroup && !run) {
|
|
36
|
+
run = [];
|
|
37
|
+
groups.push({
|
|
38
|
+
items: run,
|
|
39
|
+
label: ""
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
run?.push(item);
|
|
43
|
+
}
|
|
44
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
45
|
+
for (const option of options) {
|
|
46
|
+
const key = valueKey(itemValue(option));
|
|
47
|
+
if (!byKey.has(key)) byKey.set(key, option);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
byKey,
|
|
51
|
+
items: hasGroup ? groups : items,
|
|
52
|
+
options
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `Object.is`, then `isItemEqualToValue`, which never sees an empty value.
|
|
57
|
+
*/
|
|
58
|
+
function isSameValue(a, b, isItemEqualToValue) {
|
|
59
|
+
if (Object.is(a, b)) return true;
|
|
60
|
+
if (a === null || a === void 0 || b === null || b === void 0) return false;
|
|
61
|
+
return isItemEqualToValue?.(a, b) ?? false;
|
|
62
|
+
}
|
|
63
|
+
/** The option whose value is `value`, if `items` has it. */
|
|
64
|
+
function findOption(list, value, isItemEqualToValue) {
|
|
65
|
+
const hit = list.byKey.get(valueKey(value));
|
|
66
|
+
if (hit !== void 0 && isSameValue(itemValue(hit), value, isItemEqualToValue)) return hit;
|
|
67
|
+
return list.options.find((option) => isSameValue(itemValue(option), value, isItemEqualToValue));
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { findOption, isItemGroup, isItemObject, isSameValue, itemLabel, itemValue, toItemList, valueKey };
|
|
@@ -102,6 +102,22 @@ function settleEmptied(emptied, failing, probe, required, cleared) {
|
|
|
102
102
|
next
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
|
+
function clearNullableObjects(schema, filled, probe, required, cleared) {
|
|
106
|
+
for (const fill of filled) {
|
|
107
|
+
if (isEmptyProbe(fill.value) || cleared.has(fill.path)) continue;
|
|
108
|
+
fillAt(probe, fill.keys, null, true);
|
|
109
|
+
const issues = validateSync(schema, probe);
|
|
110
|
+
fillAt(probe, fill.keys, fill.value, true);
|
|
111
|
+
const rejected = issues?.some((issue) => {
|
|
112
|
+
const path = joinPath(issue.path?.map(pathKey) ?? []);
|
|
113
|
+
return path === fill.path || path.startsWith(`${fill.path}.`) || path.startsWith(`${fill.path}[`);
|
|
114
|
+
});
|
|
115
|
+
if (issues && !rejected) {
|
|
116
|
+
required.delete(fill.path);
|
|
117
|
+
cleared.add(fill.path);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
105
121
|
function getRequiredPaths(schema) {
|
|
106
122
|
const required = /* @__PURE__ */ new Set();
|
|
107
123
|
const cleared = /* @__PURE__ */ new Set();
|
|
@@ -111,6 +127,7 @@ function getRequiredPaths(schema) {
|
|
|
111
127
|
const issues = validateSync(schema, probe);
|
|
112
128
|
if (!issues) return required;
|
|
113
129
|
const { failing, filled } = recordPass(issues, probe, required, cleared);
|
|
130
|
+
clearNullableObjects(schema, filled, probe, required, cleared);
|
|
114
131
|
const settled = settleEmptied(emptied, failing, probe, required, cleared);
|
|
115
132
|
emptied = [...filled.filter(({ value }) => isEmptyProbe(value)), ...settled.next];
|
|
116
133
|
if (filled.length === 0 && !settled.descended) break;
|