notform 2.0.0-alpha.2 → 2.0.0-alpha.3

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/index.d.ts CHANGED
@@ -2749,7 +2749,6 @@ type DeepPartial<TData> = PartialDeep<TData, {
2749
2749
  */
2750
2750
  type Paths<TReference> = Extract<Paths$1<TReference, {
2751
2751
  maxRecursionDepth: 10;
2752
- bracketNotation: true;
2753
2752
  }>, string> | (string & {});
2754
2753
  /**
2755
2754
  * Represents a validation schema for object-based data structures.
@@ -2800,7 +2799,7 @@ type UseNotFormConfig<TSchema extends ObjectSchema> = {
2800
2799
  * The core state and methods provided by a form instance.
2801
2800
  * @template TSchema The validation schema type derived from ObjectSchema.
2802
2801
  */
2803
- type NotFormInstance<TSchema extends ObjectSchema> = Omit<UseNotFormConfig<TSchema>, 'schema' | 'onSubmit'> & {
2802
+ type NotFormInstance<TSchema extends ObjectSchema> = {
2804
2803
  /**
2805
2804
  * A convenience self-reference to the form instance.
2806
2805
  * Useful when you prefer to destructure the composable return value but still need
@@ -2809,7 +2808,14 @@ type NotFormInstance<TSchema extends ObjectSchema> = Omit<UseNotFormConfig<TSche
2809
2808
  * const { values, submit, instance } = useNotForm({ schema, onSubmit })
2810
2809
  * // <NotForm :form="instance" />
2811
2810
  */
2812
- instance: NotFormInstance<TSchema>; /** Deeply reactive object of field values */
2811
+ instance: NotFormInstance<TSchema>; /** The initial values the form was created or last reset with */
2812
+ readonly initialValues: UseNotFormConfig<TSchema>['initialValues']; /** The initial errors the form was created or last reset with */
2813
+ readonly initialErrors: UseNotFormConfig<TSchema>['initialErrors'];
2814
+ /**
2815
+ * The validation triggers of the form.
2816
+ * @default { onBlur: true, onChange: true, onInput: true }
2817
+ */
2818
+ readonly validateOn: UseNotFormConfig<TSchema>['validateOn']; /** Deeply reactive object of field values */
2813
2819
  values: Ref<StandardSchemaV1.InferInput<TSchema>>;
2814
2820
  /**
2815
2821
  * Updates a specific field value by path.
@@ -2875,14 +2881,8 @@ type NotFormInstance<TSchema extends ObjectSchema> = Omit<UseNotFormConfig<TSche
2875
2881
  * Returns all validation issues for a specific field path.
2876
2882
  * @param path Dot-separated path to the field.
2877
2883
  */
2878
- getFieldErrors: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => StandardSchemaV1.Issue[];
2879
- /**
2880
- * Reactive set of field paths currently being validated.
2881
- * Empty when no validation is in progress.
2882
- * Populated with all paths during full-form validation.
2883
- */
2884
- validatingFields: Ref<Set<Paths<StandardSchemaV1.InferInput<TSchema>>>>; /** Whether any field or the full form is currently being validated */
2885
- isValidating: ComputedRef<boolean>;
2884
+ getFieldErrors: (path: Paths<StandardSchemaV1.InferInput<TSchema>>) => StandardSchemaV1.Issue[]; /** Whether any field or the full form is currently being validated */
2885
+ isValidating: Ref<boolean>;
2886
2886
  /**
2887
2887
  * Validates the entire form against the schema.
2888
2888
  * @returns A promise resolving to the validation result.
@@ -2924,6 +2924,9 @@ type NotFormSlots = {
2924
2924
  /** The default slot */default: [];
2925
2925
  };
2926
2926
  //#endregion
2927
+ //#region src/composables/use-not-form.d.ts
2928
+ declare function useNotForm<TSchema extends ObjectSchema>(config: UseNotFormConfig<TSchema>): NotFormInstance<TSchema>;
2929
+ //#endregion
2927
2930
  //#region src/components/not-form.vue.d.ts
2928
2931
  declare const __VLS_export$1: <TSchema extends ObjectSchema>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal$1<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
2929
2932
  props: _$vue.PublicProps & __VLS_PrettifyLocal$1<NotFormProps<TSchema>> & (typeof globalThis extends {
@@ -2940,39 +2943,69 @@ declare const _default$1: typeof __VLS_export$1;
2940
2943
  type __VLS_PrettifyLocal$1<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
2941
2944
  //#endregion
2942
2945
  //#region src/types/not-field.d.ts
2946
+ /** The event handlers provided by a field instance for binding to native or custom inputs. */
2947
+ type NotFieldEvents = {
2948
+ /** Triggered when the field loses focus */onBlur: () => void; /** Triggered on every keystroke or value change */
2949
+ onInput: () => void; /** Triggered when the field value is committed */
2950
+ onChange: () => void; /** Triggered when the field gains focus */
2951
+ onFocus: () => void;
2952
+ };
2943
2953
  /**
2944
2954
  * Props for the NotField component
2945
2955
  * @template TSchema The validation schema type derived from ObjectSchema.
2956
+ * @template TPath The dot-separated path to the field within the form state.
2946
2957
  */
2947
- type NotFieldProps<TSchema extends ObjectSchema> = {
2948
- /** The unique name/path identifying the field within the form state */path: string; /** The optional form instance to use - takes priority over injected context */
2958
+ type NotFieldProps<TSchema extends ObjectSchema, TPath extends Paths<StandardSchemaV1.InferInput<TSchema>>> = {
2959
+ /** The dot-separated path to the field within the form state */path: TPath; /** Optional form instance takes priority over injected context */
2949
2960
  form?: NotFormInstance<TSchema>;
2961
+ /**
2962
+ * Per-field validation trigger overrides.
2963
+ * Merged over the form-wide validateOn config — only the keys you specify are overridden.
2964
+ */
2965
+ validateOn?: Partial<Record<ValidationTrigger, boolean>>;
2950
2966
  };
2951
2967
  /**
2952
2968
  * The core state and methods provided by a field instance.
2953
2969
  * @template TSchema The validation schema type derived from ObjectSchema.
2970
+ * @template TPath The dot-separated path to the field within the form state.
2954
2971
  */
2955
- type NotFieldInstance<TSchema extends ObjectSchema> = {
2956
- /** The unique name/path identifying the field within the form state */path: string; /** The errors of the field */
2957
- errors: StandardSchemaV1.Issue[]; /** The validate method of the field */
2958
- validate: () => ReturnType<NotFormInstance<TSchema>['validateField']>;
2972
+ type NotFieldInstance<TSchema extends ObjectSchema, TPath extends Paths<StandardSchemaV1.InferInput<TSchema>>> = {
2973
+ /** The dot-separated path to the field within the form state */path: TPath; /** The current value of the field */
2974
+ value: any; /** Whether the field is currently being validated */
2975
+ isValidating: boolean; /** Validates this field against the form schema */
2976
+ validate: () => ReturnType<NotFormInstance<TSchema>['validateField']>; /** The validation issues for this field */
2977
+ errors: StandardSchemaV1.Issue[]; /** Marks the field as touched */
2978
+ touch: () => void; /** Marks the field as not touched */
2979
+ unTouch: () => void; /** Marks the field as dirty */
2980
+ dirty: () => void; /** Marks the field as not dirty */
2981
+ unDirty: () => void;
2982
+ /**
2983
+ * All event handlers combined — spread directly onto native inputs.
2984
+ * @example <input v-bind="events" />
2985
+ */
2986
+ events: NotFieldEvents; /** Triggered when the field loses focus */
2987
+ onBlur: NotFieldEvents['onBlur']; /** Triggered on every keystroke or value change */
2988
+ onInput: NotFieldEvents['onInput']; /** Triggered when the field value is committed */
2989
+ onChange: NotFieldEvents['onChange']; /** Triggered when the field gains focus */
2990
+ onFocus: NotFieldEvents['onFocus'];
2959
2991
  };
2960
2992
  /**
2961
2993
  * Slots for the NotField component
2962
2994
  * @template TSchema The validation schema type derived from ObjectSchema.
2995
+ * @template TPath The dot-separated path to the field within the form state.
2963
2996
  */
2964
- type NotFieldSlots<TSchema extends ObjectSchema> = {
2965
- /** The default slot receives the field instance for use within templates */default: (props: NotFieldInstance<TSchema>) => [];
2997
+ type NotFieldSlots<TSchema extends ObjectSchema, TPath extends Paths<StandardSchemaV1.InferInput<TSchema>>> = {
2998
+ /** The default slot receives the full field instance */default: (props: NotFieldInstance<TSchema, TPath>) => [];
2966
2999
  };
2967
3000
  //#endregion
2968
3001
  //#region src/components/not-field.vue.d.ts
2969
- declare const __VLS_export: <TSchema extends ObjectSchema>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
2970
- props: _$vue.PublicProps & __VLS_PrettifyLocal<NotFieldProps<TSchema>> & (typeof globalThis extends {
3002
+ declare const __VLS_export: <TSchema extends ObjectSchema, TPath extends Paths<StandardSchemaV1.InferInput<TSchema>>>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
3003
+ props: _$vue.PublicProps & __VLS_PrettifyLocal<NotFieldProps<TSchema, TPath>> & (typeof globalThis extends {
2971
3004
  __VLS_PROPS_FALLBACK: infer P;
2972
3005
  } ? P : {});
2973
3006
  expose: (exposed: {}) => void;
2974
3007
  attrs: any;
2975
- slots: NotFieldSlots<TSchema>;
3008
+ slots: NotFieldSlots<TSchema, TPath>;
2976
3009
  emit: {};
2977
3010
  }>) => _$vue.VNode & {
2978
3011
  __ctx?: Awaited<typeof __VLS_setup>;
@@ -2980,4 +3013,4 @@ declare const __VLS_export: <TSchema extends ObjectSchema>(__VLS_props: NonNulla
2980
3013
  declare const _default: typeof __VLS_export;
2981
3014
  type __VLS_PrettifyLocal<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
2982
3015
  //#endregion
2983
- export { ArraySchema, DeepPartial, _default as NotField, NotFieldInstance, NotFieldProps, NotFieldSlots, _default$1 as NotForm, NotFormInstance, NotFormProps, NotFormSlots, ObjectSchema, Paths, UseNotFormConfig, ValidationTrigger };
3016
+ export { ArraySchema, DeepPartial, _default as NotField, NotFieldEvents, NotFieldInstance, NotFieldProps, NotFieldSlots, _default$1 as NotForm, NotFormInstance, NotFormProps, NotFormSlots, ObjectSchema, Paths, UseNotFormConfig, ValidationTrigger, useNotForm };
package/dist/index.js CHANGED
@@ -1,4 +1,207 @@
1
- import { computed, createElementBlock, defineComponent, guardReactiveProps, inject, normalizeProps, openBlock, provide, reactive, renderSlot, unref, useAttrs } from "vue";
1
+ import { computed, createElementBlock, defineComponent, guardReactiveProps, inject, nextTick, normalizeProps, onMounted, openBlock, provide, reactive, ref, renderSlot, toValue, unref, useAttrs } from "vue";
2
+ import { klona } from "klona/full";
3
+ import { dequal } from "dequal";
4
+ import { deepKeys, getProperty, parsePath, setProperty } from "dot-prop";
5
+ //#region src/utils/form-utils.ts
6
+ /**
7
+ * Normalizes a validation path segment into a standard property key.
8
+ * @param segment The path segment to normalize.
9
+ * @returns The normalized key.
10
+ */
11
+ function normalizeSegment(segment) {
12
+ if (typeof segment === "object" && segment !== null && "key" in segment) return segment.key;
13
+ return segment;
14
+ }
15
+ /**
16
+ * Checks if a validation issue path matches a target field path.
17
+ * @param issuePath The path array from the validation issue.
18
+ * @param targetPath The normalized path to compare against.
19
+ * @returns True if the paths are equivalent.
20
+ */
21
+ function isIssuePathEqual(issuePath, targetPath) {
22
+ if (!issuePath) return false;
23
+ if (issuePath.length !== targetPath.length) return false;
24
+ return issuePath.every((segment, index) => {
25
+ const normalizedSegment = normalizeSegment(segment);
26
+ const targetSegment = targetPath[index];
27
+ if (typeof normalizedSegment === "number" || typeof targetSegment === "number") return Number(normalizedSegment) === Number(targetSegment);
28
+ return normalizedSegment === targetSegment;
29
+ });
30
+ }
31
+ //#endregion
32
+ //#region src/composables/use-not-form.ts
33
+ function useNotForm(config) {
34
+ let initialValues = klona(config.initialValues ?? {});
35
+ let initialErrors = klona(config.initialErrors ?? []);
36
+ const validateOn = {
37
+ onBlur: config.validateOn?.onBlur ?? true,
38
+ onChange: config.validateOn?.onChange ?? true,
39
+ onInput: config.validateOn?.onInput ?? true,
40
+ onMount: config.validateOn?.onMount ?? false,
41
+ onFocus: config.validateOn?.onFocus ?? false
42
+ };
43
+ const values = ref(klona(initialValues));
44
+ const errors = ref([...initialErrors]);
45
+ const touchedFields = ref(/* @__PURE__ */ new Set());
46
+ const dirtyFields = ref(/* @__PURE__ */ new Set());
47
+ const isSubmitting = ref(false);
48
+ const isValidating = ref(false);
49
+ const errorsMap = computed(() => {
50
+ return errors.value.reduce((accumulator, issue) => {
51
+ const path = issue.path?.join(".");
52
+ if (path && !accumulator[path]) accumulator[path] = issue.message;
53
+ return accumulator;
54
+ }, {});
55
+ });
56
+ const isDirty = computed(() => dirtyFields.value.size > 0);
57
+ const isTouched = computed(() => touchedFields.value.size > 0);
58
+ const isValid = computed(() => errors.value.length === 0);
59
+ const touchField = (path) => {
60
+ touchedFields.value.add(path);
61
+ };
62
+ const unTouchField = (path) => {
63
+ touchedFields.value.delete(path);
64
+ };
65
+ const touchAllFields = () => {
66
+ deepKeys(values.value).forEach((path) => touchedFields.value.add(path));
67
+ };
68
+ const unTouchAllFields = () => {
69
+ touchedFields.value.clear();
70
+ };
71
+ const dirtyField = (path) => {
72
+ dirtyFields.value.add(path);
73
+ };
74
+ const unDirtyField = (path) => {
75
+ dirtyFields.value.delete(path);
76
+ };
77
+ const dirtyAllFields = () => {
78
+ deepKeys(values.value).forEach((path) => dirtyFields.value.add(path));
79
+ };
80
+ const unDirtyAllFields = () => {
81
+ dirtyFields.value.clear();
82
+ };
83
+ const setValue = (path, value) => {
84
+ setProperty(values.value, path, value);
85
+ touchField(path);
86
+ if (dequal(value, getProperty(initialValues, path))) unDirtyField(path);
87
+ else dirtyField(path);
88
+ if (validateOn.onChange) validateField(path);
89
+ };
90
+ const setValues = (newValues) => {
91
+ for (const [path, value] of Object.entries(newValues)) setValue(path, value);
92
+ };
93
+ const setError = (newIssue) => {
94
+ const newPath = newIssue.path?.join(".");
95
+ errors.value = [...errors.value.filter((error) => error.path?.join(".") !== newPath), newIssue];
96
+ };
97
+ const setErrors = (newIssues) => {
98
+ errors.value = [...newIssues];
99
+ };
100
+ const clearErrors = () => {
101
+ errors.value = [];
102
+ };
103
+ const getFieldErrors = (path) => {
104
+ const pathArray = parsePath(path);
105
+ return errors.value.filter((error) => isIssuePathEqual(error.path, pathArray));
106
+ };
107
+ const validate = async () => {
108
+ isValidating.value = true;
109
+ try {
110
+ const result = await toValue(config.schema)["~standard"].validate(values.value);
111
+ if (result?.issues) {
112
+ setErrors([...result.issues]);
113
+ return { issues: result.issues };
114
+ }
115
+ clearErrors();
116
+ return { value: result.value };
117
+ } finally {
118
+ isValidating.value = false;
119
+ }
120
+ };
121
+ const validateField = async (path) => {
122
+ isValidating.value = true;
123
+ try {
124
+ const result = await toValue(config.schema)["~standard"].validate(values.value);
125
+ const pathArray = parsePath(path);
126
+ errors.value = errors.value.filter((error) => !isIssuePathEqual(error.path, pathArray));
127
+ if (result?.issues) {
128
+ const fieldIssues = result.issues.filter((issue) => isIssuePathEqual(issue.path, pathArray));
129
+ if (fieldIssues.length > 0) {
130
+ errors.value = [...errors.value, ...fieldIssues];
131
+ return { issues: fieldIssues };
132
+ }
133
+ return { value: getProperty(values.value, path) };
134
+ }
135
+ return { value: getProperty(result.value, path) };
136
+ } finally {
137
+ isValidating.value = false;
138
+ }
139
+ };
140
+ const submit = async (event) => {
141
+ isSubmitting.value = true;
142
+ try {
143
+ touchAllFields();
144
+ dirtyAllFields();
145
+ const result = await validate();
146
+ if (result?.issues) {
147
+ event.preventDefault();
148
+ return;
149
+ }
150
+ if (config.onSubmit) {
151
+ event.preventDefault();
152
+ await config.onSubmit(result.value);
153
+ }
154
+ } catch {
155
+ event.preventDefault();
156
+ } finally {
157
+ isSubmitting.value = false;
158
+ }
159
+ };
160
+ const reset = (newValues, newErrors) => {
161
+ if (newValues) initialValues = klona(newValues);
162
+ if (newErrors) initialErrors = klona(newErrors);
163
+ values.value = klona(initialValues);
164
+ errors.value = klona(initialErrors);
165
+ touchedFields.value.clear();
166
+ dirtyFields.value.clear();
167
+ };
168
+ const resolvedInstance = {
169
+ initialValues,
170
+ initialErrors,
171
+ validateOn,
172
+ values,
173
+ setValue,
174
+ setValues,
175
+ touchedFields,
176
+ touchField,
177
+ unTouchField,
178
+ touchAllFields,
179
+ unTouchAllFields,
180
+ isTouched,
181
+ dirtyFields,
182
+ dirtyField,
183
+ unDirtyField,
184
+ dirtyAllFields,
185
+ unDirtyAllFields,
186
+ isDirty,
187
+ errors,
188
+ errorsMap,
189
+ setError,
190
+ setErrors,
191
+ clearErrors,
192
+ getFieldErrors,
193
+ isValidating,
194
+ validate,
195
+ validateField,
196
+ isValid,
197
+ isSubmitting,
198
+ submit,
199
+ reset
200
+ };
201
+ resolvedInstance.instance = resolvedInstance;
202
+ return resolvedInstance;
203
+ }
204
+ //#endregion
2
205
  //#region src/utils/instance-utils.ts
3
206
  /** Injection key for the current active form instance */
4
207
  const NOT_FORM_INSTANCE_KEY = Symbol("notform:instance");
@@ -17,7 +220,7 @@ function provideNotFormInstance(instance) {
17
220
  function useNotFormInstance(explicitInstance) {
18
221
  const injected = inject(NOT_FORM_INSTANCE_KEY);
19
222
  const instance = explicitInstance ?? injected;
20
- if (!instance) throw new Error("[NotForm] No form instance found. Add a <NotForm :form=\"...\"> ancestor or pass :form directly.");
223
+ if (!instance) throw new Error("[NotForm] No form instance found. Add a <NotForm :instance=\"...\"> ancestor or pass :form directly.");
21
224
  return instance;
22
225
  }
23
226
  //#endregion
@@ -43,24 +246,78 @@ var not_field_default = /* @__PURE__ */ defineComponent({
43
246
  __name: "not-field",
44
247
  props: {
45
248
  path: {
46
- type: String,
249
+ type: null,
47
250
  required: true
48
251
  },
49
252
  form: {
50
253
  type: Object,
51
254
  required: false
255
+ },
256
+ validateOn: {
257
+ type: Object,
258
+ required: false
52
259
  }
53
260
  },
54
261
  setup(__props) {
55
262
  const props = __props;
56
263
  const formInstance = useNotFormInstance(props.form);
264
+ const isValidating = ref(false);
57
265
  const path = computed(() => props.path);
58
- const errors = formInstance.getFieldErrors(path.value);
59
- const validate = () => formInstance.validateField(path.value);
266
+ const value = computed(() => getProperty(formInstance.values.value, props.path));
267
+ const errors = computed(() => formInstance.getFieldErrors(path.value));
268
+ const touch = () => formInstance.touchField(path.value);
269
+ const unTouch = () => formInstance.unTouchField(path.value);
270
+ const dirty = () => formInstance.dirtyField(path.value);
271
+ const unDirty = () => formInstance.unDirtyField(path.value);
272
+ const validate = async () => {
273
+ isValidating.value = true;
274
+ try {
275
+ return await formInstance.validateField(path.value);
276
+ } finally {
277
+ isValidating.value = false;
278
+ }
279
+ };
280
+ const validateOn = computed(() => ({
281
+ ...formInstance.validateOn,
282
+ ...props.validateOn
283
+ }));
284
+ const onBlur = () => {
285
+ touch();
286
+ if (validateOn.value.onBlur) validate();
287
+ };
288
+ const onChange = () => {
289
+ if (validateOn.value.onChange) validate();
290
+ };
291
+ const onInput = () => {
292
+ if (validateOn.value.onInput) validate();
293
+ };
294
+ const onFocus = () => {
295
+ if (validateOn.value.onFocus) validate();
296
+ };
60
297
  const fieldInstance = reactive({
61
- path: path.value,
298
+ path,
299
+ value,
300
+ isValidating,
301
+ validate,
62
302
  errors,
63
- validate
303
+ touch,
304
+ unTouch,
305
+ dirty,
306
+ unDirty,
307
+ events: computed(() => ({
308
+ onBlur,
309
+ onInput,
310
+ onChange,
311
+ onFocus
312
+ })),
313
+ onBlur,
314
+ onInput,
315
+ onChange,
316
+ onFocus
317
+ });
318
+ onMounted(async () => {
319
+ await nextTick();
320
+ if (validateOn.value.onMount) validate();
64
321
  });
65
322
  return (_ctx, _cache) => {
66
323
  return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(fieldInstance)));
@@ -68,4 +325,4 @@ var not_field_default = /* @__PURE__ */ defineComponent({
68
325
  }
69
326
  });
70
327
  //#endregion
71
- export { not_field_default as NotField, not_form_default as NotForm };
328
+ export { not_field_default as NotField, not_form_default as NotForm, useNotForm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notform",
3
- "version": "2.0.0-alpha.2",
3
+ "version": "2.0.0-alpha.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Vue Forms Without the Friction",