notform 2.0.0-beta.1 → 2.0.0-beta.2

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
@@ -260,6 +260,27 @@ type NotFieldProps = {
260
260
  * ```
261
261
  */
262
262
  validateOn?: Partial<Record<ValidationTrigger, boolean>>;
263
+ /**
264
+ * Debounce delay in milliseconds for input- and change-triggered validation.
265
+ *
266
+ * When set, validation is deferred until the user stops typing for the given
267
+ * duration. Only the final call within the window runs — earlier ones are
268
+ * cancelled. Useful for async validators (e.g. username availability checks)
269
+ * where firing on every keystroke would cause excessive requests.
270
+ *
271
+ * Blur- and submit-triggered validation always runs immediately, regardless
272
+ * of this setting, so the field never feels unresponsive when the user leaves.
273
+ *
274
+ * ```vue
275
+ * <!-- validate 400ms after the user stops typing -->
276
+ * <NotField path="username" :debounce="400" v-slot="{ events }">
277
+ * <input v-model="form.values.username" v-bind="events" />
278
+ * </NotField>
279
+ * ```
280
+ *
281
+ * Omit or set to `0` to disable debouncing (default behaviour).
282
+ */
283
+ debounce?: number;
263
284
  };
264
285
  /**
265
286
  * Everything available inside the `NotField` default slot.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
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, watch, withCtx } from "vue";
1
+ import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, defineComponent, guardReactiveProps, inject, markRaw, mergeProps, nextTick, normalizeProps, onMounted, onUnmounted, openBlock, provide, reactive, ref, renderSlot, resolveDynamicComponent, toDisplayString, toValue, unref, useAttrs, watch, 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";
@@ -310,6 +310,10 @@ var not_field_default = /* @__PURE__ */ defineComponent({
310
310
  validateOn: {
311
311
  type: Object,
312
312
  required: false
313
+ },
314
+ debounce: {
315
+ type: Number,
316
+ required: false
313
317
  }
314
318
  },
315
319
  setup(__props) {
@@ -320,6 +324,36 @@ var not_field_default = /* @__PURE__ */ defineComponent({
320
324
  ...props.validateOn
321
325
  }));
322
326
  const isValidating = ref(false);
327
+ /** Timer handle for the current pending debounced validation, if any. */
328
+ let debounceTimer;
329
+ /**
330
+ * Cancels any pending debounced validation without running it.
331
+ * Called on blur (so blur's own immediate validation takes over) and on unmount
332
+ * (to prevent a timer from firing after the component is gone).
333
+ */
334
+ const clearDebounce = () => {
335
+ if (debounceTimer !== void 0) {
336
+ clearTimeout(debounceTimer);
337
+ debounceTimer = void 0;
338
+ }
339
+ };
340
+ /**
341
+ * Schedules a validation run, respecting the field's `debounce` prop.
342
+ *
343
+ * - If `debounce` is `0` or omitted, validation runs synchronously.
344
+ * - Otherwise, any pending timer is cancelled and a new one is started.
345
+ * Only the final call within the window actually validates — useful for
346
+ * async checks (availability lookups, server-side rules) where firing on
347
+ * every keystroke would be wasteful.
348
+ */
349
+ const scheduleValidation = () => {
350
+ if (!props.debounce) {
351
+ validate();
352
+ return;
353
+ }
354
+ clearDebounce();
355
+ debounceTimer = setTimeout(validate, props.debounce);
356
+ };
323
357
  const value = computed(() => getProperty(form.values, props.path));
324
358
  const errors = computed(() => form.getFieldErrors(props.path));
325
359
  const isValid = computed(() => errors.value.length === 0);
@@ -343,21 +377,22 @@ var not_field_default = /* @__PURE__ */ defineComponent({
343
377
  }
344
378
  };
345
379
  const onBlur = () => {
380
+ clearDebounce();
346
381
  form.touchField(props.path);
347
382
  if (validateOn.value.onBlur) validate();
348
383
  };
349
384
  const onInput = () => {
350
385
  updateDirty();
351
386
  if (!validateOn.value.onInput) return;
352
- if (form.validationMode.eager && errors.value.length > 0) validate();
387
+ if (form.validationMode.eager && !isValid.value) scheduleValidation();
353
388
  };
354
389
  const onChange = () => {
355
390
  updateDirty();
356
391
  if (!validateOn.value.onChange) return;
357
- if (form.validationMode.eager && errors.value.length > 0) validate();
392
+ if (form.validationMode.eager && !isValid.value) scheduleValidation();
358
393
  };
359
394
  const onFocus = () => {
360
- if (validateOn.value.onFocus) validate();
395
+ if (validateOn.value.onFocus) scheduleValidation();
361
396
  };
362
397
  const events = computed(() => ({
363
398
  onBlur,
@@ -369,6 +404,7 @@ var not_field_default = /* @__PURE__ */ defineComponent({
369
404
  await nextTick();
370
405
  if (validateOn.value.onMount) validate();
371
406
  });
407
+ onUnmounted(clearDebounce);
372
408
  const slotProps = computed(() => ({
373
409
  path: props.path,
374
410
  value: value.value,
@@ -497,11 +533,7 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
497
533
  isValidating.value = false;
498
534
  }
499
535
  };
500
- /**
501
- * Re-aligns itemKeys with the current array length.
502
- * Called at the start of every mutation so keys stay consistent
503
- * after external changes (e.g. form.reset()).
504
- */
536
+ /** Re-aligns itemKeys with the current array length. */
505
537
  const syncKeys = () => {
506
538
  const arrayLength = array.value.length;
507
539
  if (itemKeys.value.length > arrayLength) itemKeys.value.length = arrayLength;
@@ -513,7 +545,6 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
513
545
  * automatically via the computed above so no explicit dirty call is needed here.
514
546
  */
515
547
  const mutate = (updater) => {
516
- syncKeys();
517
548
  const current = [...array.value];
518
549
  updater(current);
519
550
  setProperty(form.values, props.path, current);
@@ -562,21 +593,16 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
562
593
  if (validateOn.value.onMount) validate();
563
594
  });
564
595
  /**
565
- * Syncs `itemKeys` whenever the underlying array changes outside of this
566
- * component's own mutation methods — most commonly after `form.reset()`.
567
- *
568
- * Without this, `itemKeys` would be stale until the next mutation:
569
- * - If the reset shrinks the array, a subsequent `append` would call
570
- * `syncKeys` and regenerate keys for the surviving items, causing
571
- * Vue to remount them unnecessarily.
572
- * - If the reset grows the array, items render with `fallback` keys
573
- * until the next mutation, breaking key stability guarantees.
596
+ * Syncs `itemKeys` when the array length changes outside of this component's
597
+ * own mutation methods — most commonly after `form.reset()`.
574
598
  *
575
- * Our own mutations already manage keys explicitly before writing to
576
- * the array, so when this watcher fires after them, `syncKeys` is a
577
- * cheap no-op (lengths already match).
599
+ * Watching `length` rather than the full array avoids unnecessary syncs on
600
+ * `update()` and `swap()`, which mutate values but never add or remove items.
601
+ * When our own mutations run, they manage keys explicitly before writing to
602
+ * the array, so by the time this watcher fires the lengths already match and
603
+ * `syncKeys` is a cheap no-op.
578
604
  */
579
- watch(array, () => syncKeys());
605
+ watch(() => array.value.length, syncKeys);
580
606
  const slotProps = computed(() => ({
581
607
  path: props.path,
582
608
  items: items.value,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notform",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Vue Forms Without the Friction",