notform 2.0.0-alpha.5 → 2.0.0-alpha.6

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +144 -20
  2. package/dist/index.js +165 -10
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -45,17 +45,6 @@ type ObjectSchema = StandardSchemaV1 & {
45
45
  };
46
46
  };
47
47
  };
48
- /**
49
- * Represents a validation schema for array-based data structures.
50
- * Complies with the Standard Schema specification.
51
- */
52
- type ArraySchema = StandardSchemaV1 & {
53
- '~standard': StandardSchemaV1['~standard'] & {
54
- types?: {
55
- input: Array<any>;
56
- };
57
- };
58
- };
59
48
  //#endregion
60
49
  //#region src/types/use-not-form.d.ts
61
50
  /**
@@ -219,8 +208,8 @@ declare function useNotForm<TSchema extends ObjectSchema>(config: UseNotFormConf
219
208
  //#region src/components/not-form.vue.d.ts
220
209
  type __VLS_Slots$1 = NotFormSlots;
221
210
  declare const __VLS_base$1: _$vue.DefineComponent<NotFormProps, {}, {}, {}, {}, _$vue.ComponentOptionsMixin, _$vue.ComponentOptionsMixin, {}, string, _$vue.PublicProps, Readonly<NotFormProps> & Readonly<{}>, {}, {}, {}, {}, string, _$vue.ComponentProvideOptions, false, {}, any>;
222
- declare const __VLS_export$2: __VLS_WithSlots$1<typeof __VLS_base$1, __VLS_Slots$1>;
223
- declare const _default$1: typeof __VLS_export$2;
211
+ declare const __VLS_export$3: __VLS_WithSlots$1<typeof __VLS_base$1, __VLS_Slots$1>;
212
+ declare const _default$2: typeof __VLS_export$3;
224
213
  type __VLS_WithSlots$1<T, S> = T & {
225
214
  new (): {
226
215
  $slots: S;
@@ -316,8 +305,8 @@ type NotFieldSlots<TSchema extends ObjectSchema> = {
316
305
  };
317
306
  //#endregion
318
307
  //#region src/components/not-field.vue.d.ts
319
- declare const __VLS_export$1: <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<{
320
- props: _$vue.PublicProps & __VLS_PrettifyLocal<NotFieldProps> & (typeof globalThis extends {
308
+ declare const __VLS_export$2: <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<{
309
+ props: _$vue.PublicProps & __VLS_PrettifyLocal$1<NotFieldProps> & (typeof globalThis extends {
321
310
  __VLS_PROPS_FALLBACK: infer P;
322
311
  } ? P : {});
323
312
  expose: (exposed: {}) => void;
@@ -327,8 +316,8 @@ declare const __VLS_export$1: <TSchema extends ObjectSchema>(__VLS_props: NonNul
327
316
  }>) => _$vue.VNode & {
328
317
  __ctx?: Awaited<typeof __VLS_setup>;
329
318
  };
330
- declare const _default: typeof __VLS_export$1;
331
- type __VLS_PrettifyLocal<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
319
+ declare const _default$1: typeof __VLS_export$2;
320
+ type __VLS_PrettifyLocal$1<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
332
321
  //#endregion
333
322
  //#region src/types/not-message.d.ts
334
323
  /** Props for the `NotMessage` component. */
@@ -361,12 +350,147 @@ type __VLS_Slots = NotMessageSlots;
361
350
  declare const __VLS_base: _$vue.DefineComponent<NotMessageProps, {}, {}, {}, {}, _$vue.ComponentOptionsMixin, _$vue.ComponentOptionsMixin, {}, string, _$vue.PublicProps, Readonly<NotMessageProps> & Readonly<{}>, {
362
351
  as: string;
363
352
  }, {}, {}, {}, string, _$vue.ComponentProvideOptions, false, {}, any>;
364
- declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
365
- declare const _default$2: typeof __VLS_export;
353
+ declare const __VLS_export$1: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
354
+ declare const _default$3: typeof __VLS_export$1;
366
355
  type __VLS_WithSlots<T, S> = T & {
367
356
  new (): {
368
357
  $slots: S;
369
358
  };
370
359
  };
371
360
  //#endregion
372
- export { ArraySchema, DeepPartial, _default as NotField, NotFieldEvents, NotFieldProps, NotFieldSlotProps, NotFieldSlots, _default$1 as NotForm, NotFormInstance, NotFormProps, NotFormSlots, _default$2 as NotMessage, NotMessageProps, NotMessageSlotProps, NotMessageSlots, ObjectSchema, Paths, UseNotFormConfig, ValidationMode, ValidationTrigger, useNotForm };
361
+ //#region src/types/not-array-field.d.ts
362
+ /** Props for the `NotArrayField` component. */
363
+ type NotArrayFieldProps<TItemSchema extends StandardSchemaV1 = StandardSchemaV1> = {
364
+ /** Dot-separated path to the array within the form values. */path: string;
365
+ /**
366
+ * Schema for a single array item — used purely for type inference.
367
+ * Enables typed `append`, `prepend`, `insert`, and `update` methods in the slot.
368
+ *
369
+ * ```vue
370
+ * <NotArrayField path="tags" :item-schema="z.string()">
371
+ * <!-- append now expects a string -->
372
+ * </NotArrayField>
373
+ * ```
374
+ */
375
+ itemSchema?: TItemSchema;
376
+ /**
377
+ * Explicit form instance override.
378
+ * Takes priority over the instance provided by a `NotForm` ancestor.
379
+ * Required when using `NotArrayField` outside of a `NotForm`.
380
+ */
381
+ form?: NotFormInstance<any>;
382
+ /**
383
+ * Per-field validation trigger overrides applied to the array as a whole.
384
+ * Merged over the form-wide `validateOn` — only the keys you specify are overridden.
385
+ */
386
+ validateOn?: Partial<Record<Extract<ValidationTrigger, 'onMount' | 'onChange'>, boolean>>;
387
+ };
388
+ /**
389
+ * Represents a single item in the array field.
390
+ * Use `key` for `v-for` tracking and `path` to pass to a nested `NotField`.
391
+ *
392
+ * ```vue
393
+ * <NotArrayField path="tags" v-slot="{ items }">
394
+ * <NotField v-for="item in items" :key="item.key" :path="item.path" v-slot="{ events }">
395
+ * <input v-model="form.values.tags[item.index]" v-bind="events" />
396
+ * </NotField>
397
+ * </NotArrayField>
398
+ * ```
399
+ */
400
+ type NotArrayFieldItem = {
401
+ /** Stable key for `v-for` — does not change when items are reordered. */key: string; /** Current index of this item in the array. */
402
+ index: number; /** Full dot-separated path to this item — pass directly to `NotField`. */
403
+ path: string;
404
+ };
405
+ /**
406
+ * Everything available inside the `NotArrayField` default slot.
407
+ * @template TSchema The form schema type.
408
+ * @template TItem The inferred type of a single array item.
409
+ */
410
+ type NotArrayFieldSlotProps<TSchema extends ObjectSchema, TItem = any> = {
411
+ /** The dot-separated path to this array field. */path: string; /** The array items with stable keys and paths for use with `v-for`. */
412
+ items: NotArrayFieldItem[]; /** The current length of the array. */
413
+ length: number; /** All validation issues for this array field from the last validation run. */
414
+ errors: StandardSchemaV1.Issue[]; /** Whether this array field currently has no validation errors. */
415
+ isValid: boolean;
416
+ /**
417
+ * Whether any item in this array has been touched.
418
+ * Derived from the form's `touchedFields` set.
419
+ */
420
+ isTouched: boolean;
421
+ /**
422
+ * Whether any item in this array differs from its initial value.
423
+ * Derived from the form's `dirtyFields` set.
424
+ */
425
+ isDirty: boolean; /** Whether validation is currently running for this array field. */
426
+ isValidating: boolean;
427
+ /**
428
+ * Manually triggers validation for this array field.
429
+ * Useful when mutations are performed programmatically outside of the normal flow.
430
+ */
431
+ validate: () => ReturnType<NotFormInstance<TSchema>['validateField']>;
432
+ /**
433
+ * Appends a new item to the end of the array.
434
+ * @param value The value to append.
435
+ */
436
+ append: (value: TItem) => void;
437
+ /**
438
+ * Prepends a new item to the beginning of the array.
439
+ * @param value The value to prepend.
440
+ */
441
+ prepend: (value: TItem) => void;
442
+ /**
443
+ * Removes the item at the given index.
444
+ * @param index The index of the item to remove.
445
+ */
446
+ remove: (index: number) => void;
447
+ /**
448
+ * Inserts a new item at the given index, shifting subsequent items forward.
449
+ * @param index The index to insert the item at.
450
+ * @param value The value to insert.
451
+ */
452
+ insert: (index: number, value: TItem) => void;
453
+ /**
454
+ * Replaces the value at the given index.
455
+ * @param index The index of the item to replace.
456
+ * @param value The value to replace with.
457
+ */
458
+ update: (index: number, value: TItem) => void;
459
+ /**
460
+ * Swaps the positions of two items in the array.
461
+ * @param indexA The index of the first item.
462
+ * @param indexB The index of the second item.
463
+ */
464
+ swap: (indexA: number, indexB: number) => void;
465
+ /**
466
+ * Moves an item from one index to another, shifting items between them.
467
+ * @param from The current index of the item.
468
+ * @param to The target index.
469
+ */
470
+ move: (from: number, to: number) => void;
471
+ };
472
+ /**
473
+ * Slots for the `NotArrayField` component.
474
+ * @template TSchema The form schema type.
475
+ * @template TItem The inferred type of a single array item.
476
+ */
477
+ type NotArrayFieldSlots<TSchema extends ObjectSchema, TItem = any> = {
478
+ /** The default slot receives the full array state and manipulation methods. */default: (props: NotArrayFieldSlotProps<TSchema, TItem>) => [];
479
+ };
480
+ //#endregion
481
+ //#region src/components/not-array-field.vue.d.ts
482
+ declare const __VLS_export: <TSchema extends ObjectSchema, TItemSchema extends StandardSchemaV1 = StandardSchemaV1, TItem = StandardSchemaV1.InferInput<TItemSchema>>(__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<{
483
+ props: _$vue.PublicProps & __VLS_PrettifyLocal<NotArrayFieldProps<TItemSchema>> & (typeof globalThis extends {
484
+ __VLS_PROPS_FALLBACK: infer P;
485
+ } ? P : {});
486
+ expose: (exposed: {}) => void;
487
+ attrs: any;
488
+ slots: NotArrayFieldSlots<TSchema, TItem>;
489
+ emit: {};
490
+ }>) => _$vue.VNode & {
491
+ __ctx?: Awaited<typeof __VLS_setup>;
492
+ };
493
+ declare const _default: typeof __VLS_export;
494
+ type __VLS_PrettifyLocal<T> = (T extends any ? { [K in keyof T]: T[K] } : { [K in keyof T as K]: T[K] }) & {};
495
+ //#endregion
496
+ export { DeepPartial, _default as NotArrayField, NotArrayFieldItem, NotArrayFieldProps, NotArrayFieldSlotProps, NotArrayFieldSlots, _default$1 as NotField, NotFieldEvents, NotFieldProps, NotFieldSlotProps, NotFieldSlots, _default$2 as NotForm, NotFormInstance, NotFormProps, NotFormSlots, _default$3 as NotMessage, NotMessageProps, NotMessageSlotProps, NotMessageSlots, ObjectSchema, Paths, UseNotFormConfig, ValidationMode, ValidationTrigger, useNotForm };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { computed, createBlock, createElementBlock, createTextVNode, defineComponent, guardReactiveProps, inject, markRaw, nextTick, normalizeProps, onMounted, openBlock, provide, reactive, ref, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, useAttrs, vShow, withCtx, withDirectives } from "vue";
1
+ import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, guardReactiveProps, inject, markRaw, mergeProps, nextTick, normalizeProps, onMounted, openBlock, provide, reactive, ref, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, useAttrs, withCtx } from "vue";
2
2
  import { klona } from "klona/full";
3
3
  import { dequal } from "dequal";
4
4
  import { deepKeys, deleteProperty, getProperty, hasProperty, parsePath, setProperty } from "dot-prop";
@@ -189,12 +189,9 @@ function useNotForm(config) {
189
189
  if (newValues) initialValues = klona(newValues);
190
190
  if (newErrors) initialErrors = klona(newErrors);
191
191
  const freshValues = klona(initialValues);
192
- deepKeys(values).forEach((path) => {
193
- if (!hasProperty(freshValues, path)) deleteProperty(values, path);
194
- });
195
- deepKeys(freshValues).forEach((path) => {
196
- setProperty(values, path, getProperty(freshValues, path));
197
- });
192
+ const current = values;
193
+ for (const key of Object.keys(current)) if (!hasProperty(freshValues, key)) deleteProperty(values, key);
194
+ for (const key of Object.keys(freshValues)) setProperty(values, key, getProperty(freshValues, key));
198
195
  errors.splice(0, errors.length, ...klona(initialErrors));
199
196
  touchedFields.clear();
200
197
  dirtyFields.clear();
@@ -394,12 +391,170 @@ var not_message_default = /* @__PURE__ */ defineComponent({
394
391
  attributes
395
392
  }));
396
393
  return (_ctx, _cache) => {
397
- return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps.value)), () => [withDirectives((openBlock(), createBlock(resolveDynamicComponent(props.as), normalizeProps(guardReactiveProps(unref(attributes))), {
394
+ return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps.value)), () => [message.value ? (openBlock(), createBlock(resolveDynamicComponent(props.as), normalizeProps(mergeProps({ key: 0 }, unref(attributes))), {
398
395
  default: withCtx(() => [createTextVNode(toDisplayString(message.value), 1)]),
399
396
  _: 1
400
- }, 16)), [[vShow, message.value]])]);
397
+ }, 16)) : createCommentVNode("v-if", true)]);
398
+ };
399
+ }
400
+ });
401
+ //#endregion
402
+ //#region src/components/not-array-field.vue
403
+ var not_array_field_default = /* @__PURE__ */ defineComponent({
404
+ inheritAttrs: false,
405
+ __name: "not-array-field",
406
+ props: {
407
+ path: {
408
+ type: String,
409
+ required: true
410
+ },
411
+ itemSchema: {
412
+ type: null,
413
+ required: false
414
+ },
415
+ form: {
416
+ type: null,
417
+ required: false
418
+ },
419
+ validateOn: {
420
+ type: Object,
421
+ required: false
422
+ }
423
+ },
424
+ setup(__props) {
425
+ const props = __props;
426
+ const form = useNotFormInstance(props.form);
427
+ const validateOn = computed(() => ({
428
+ onMount: props.validateOn?.onMount ?? form.validateOn.onMount,
429
+ onChange: props.validateOn?.onChange ?? form.validateOn.onChange
430
+ }));
431
+ const isValidating = ref(false);
432
+ /**
433
+ * Stable keys per item — survive reorders, removals, and inserts.
434
+ * Seeded from the length of the initial array so existing items have keys from the start.
435
+ */
436
+ const itemKeys = ref((() => {
437
+ const initial = getProperty(form.values, props.path);
438
+ const length = Array.isArray(initial) ? initial.length : 0;
439
+ return Array.from({ length }, (_, index) => `${props.path}-${index}-${Date.now()}`);
440
+ })());
441
+ let keyCounter = itemKeys.value.length;
442
+ const generateKey = () => `${props.path}-${keyCounter++}-${Date.now()}`;
443
+ const array = computed(() => {
444
+ const value = getProperty(form.values, props.path);
445
+ return Array.isArray(value) ? value : [];
446
+ });
447
+ const items = computed(() => array.value.map((_, index) => ({
448
+ key: itemKeys.value[index] ?? `${props.path}-fallback-${index}`,
449
+ index,
450
+ path: `${props.path}.${index}`
451
+ })));
452
+ const errors = computed(() => form.getFieldErrors(props.path));
453
+ const isValid = computed(() => errors.value.length === 0);
454
+ /**
455
+ * Derived from the form's touchedFields set.
456
+ * True if the array path itself or any of its item paths have been touched.
457
+ */
458
+ const isTouched = computed(() => form.touchedFields.has(props.path) || [...form.touchedFields].some((path) => path.startsWith(`${props.path}.`)));
459
+ /**
460
+ * Derived from the form's dirtyFields set.
461
+ * True if the array path itself or any of its item paths are dirty.
462
+ */
463
+ const isDirty = computed(() => form.dirtyFields.has(props.path) || [...form.dirtyFields].some((path) => path.startsWith(`${props.path}.`)));
464
+ const validate = async () => {
465
+ isValidating.value = true;
466
+ try {
467
+ return await form.validateField(props.path);
468
+ } finally {
469
+ isValidating.value = false;
470
+ }
471
+ };
472
+ /**
473
+ * Re-aligns itemKeys with the current array length.
474
+ * Called at the start of every mutation so keys stay consistent
475
+ * after external changes (e.g. form.reset()).
476
+ */
477
+ const syncKeys = () => {
478
+ const arrayLength = array.value.length;
479
+ if (itemKeys.value.length > arrayLength) itemKeys.value.length = arrayLength;
480
+ else while (itemKeys.value.length < arrayLength) itemKeys.value.push(generateKey());
481
+ };
482
+ /**
483
+ * Applies an update to a copy of the current array, writes it back to the form values,
484
+ * and marks the array path as touched. Dirty state is derived from the form's Sets
485
+ * automatically via the computed above so no explicit dirty call is needed here.
486
+ */
487
+ const mutate = (updater) => {
488
+ syncKeys();
489
+ const current = [...array.value];
490
+ updater(current);
491
+ setProperty(form.values, props.path, current);
492
+ form.touchField(props.path);
493
+ if (dequal(current, getProperty(form.initialValues, props.path))) form.dirtyFields.delete(props.path);
494
+ else form.dirtyField(props.path);
495
+ if (validateOn.value.onChange) validate();
496
+ };
497
+ const append = (value) => {
498
+ itemKeys.value.push(generateKey());
499
+ mutate((current) => current.push(value));
500
+ };
501
+ const prepend = (value) => {
502
+ itemKeys.value.unshift(generateKey());
503
+ mutate((current) => current.unshift(value));
504
+ };
505
+ const remove = (index) => {
506
+ mutate((current) => current.splice(index, 1));
507
+ itemKeys.value.splice(index, 1);
508
+ };
509
+ const insert = (index, value) => {
510
+ itemKeys.value.splice(index, 0, generateKey());
511
+ mutate((current) => current.splice(index, 0, value));
512
+ };
513
+ const update = (index, value) => {
514
+ mutate((current) => {
515
+ current[index] = value;
516
+ });
517
+ };
518
+ const swap = (indexA, indexB) => {
519
+ mutate((current) => {
520
+ [current[indexA], current[indexB]] = [current[indexB], current[indexA]];
521
+ });
522
+ [itemKeys.value[indexA], itemKeys.value[indexB]] = [itemKeys.value[indexB], itemKeys.value[indexA]];
523
+ };
524
+ const move = (from, to) => {
525
+ mutate((current) => {
526
+ const [item] = current.splice(from, 1);
527
+ current.splice(to, 0, item);
528
+ });
529
+ const [key] = itemKeys.value.splice(from, 1);
530
+ itemKeys.value.splice(to, 0, key);
531
+ };
532
+ onMounted(async () => {
533
+ await nextTick();
534
+ if (validateOn.value.onMount) validate();
535
+ });
536
+ const slotProps = computed(() => ({
537
+ path: props.path,
538
+ items: items.value,
539
+ length: array.value.length,
540
+ errors: errors.value,
541
+ isValid: isValid.value,
542
+ isTouched: isTouched.value,
543
+ isDirty: isDirty.value,
544
+ isValidating: isValidating.value,
545
+ validate,
546
+ append,
547
+ prepend,
548
+ remove,
549
+ insert,
550
+ update,
551
+ swap,
552
+ move
553
+ }));
554
+ return (_ctx, _cache) => {
555
+ return renderSlot(_ctx.$slots, "default", normalizeProps(guardReactiveProps(slotProps.value)));
401
556
  };
402
557
  }
403
558
  });
404
559
  //#endregion
405
- export { not_field_default as NotField, not_form_default as NotForm, not_message_default as NotMessage, useNotForm };
560
+ export { not_array_field_default as NotArrayField, not_field_default as NotField, not_form_default as NotForm, not_message_default as NotMessage, useNotForm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notform",
3
- "version": "2.0.0-alpha.5",
3
+ "version": "2.0.0-alpha.6",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Vue Forms Without the Friction",