notform 2.2.2 → 2.2.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/README.md CHANGED
@@ -1,16 +1,14 @@
1
1
  <div align="center">
2
- <h1>notform</h1>
3
- <p><strong>Vue Forms Without the Friction</strong></p>
2
+ <h1><code>notform</code></h1>
3
+ <p><strong>Headless Form Management, Seamlessly Integrated with Nuxt</strong></p>
4
4
  <p>
5
- <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/v/notform.svg?style=plastic&label=NPM%20Version&color=blue" alt="NPM Version"></a>
5
+ <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/v/notform.svg?style=plastic&label=NPM%20Version" alt="NPM Version"></a>
6
6
  <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/dt/notform.svg?style=plastic&label=NPM%20Downloads&color=blue" alt="NPM Downloads"></a>
7
7
  <a href="https://npmx.dev/package/notform"><img src="https://img.shields.io/npm/unpacked-size/notform?style=plastic&label=NPM%20Unpacked%20Size&color=blue" alt="NPM Unpacked Size"></a>
8
8
  </p>
9
9
  </div>
10
10
 
11
- <br>
12
-
13
- `notform` is the core package of the NotForm ecosystem. It provides type-safe form validation and state management for Vue 3 applications with minimal boilerplate. Built with TypeScript from the ground up, it offers a composable API that integrates perfectly with Vue 3's Composition API.
11
+ `notform` is the core package of the NotForm ecosystem. It provides headless form validation and state management for Vue 3 applications. Built with TypeScript from the ground up, it offers a composable API that integrates perfectly with Vue 3's Composition API and works with any Standard Schema-compatible validator.
14
12
 
15
13
  ## Installation
16
14
 
@@ -20,16 +18,19 @@ pnpm add notform
20
18
 
21
19
  ## How It Works
22
20
 
23
- NotForm follows a **composable-first** approach. Forms are managed through the `useNotForm` composable which handles validation, state, and submission logic in a type-safe manner.
21
+ NotForm follows a **headless, composable-first** approach. Forms are managed through the `useNotForm` composable which handles validation, state, and submission logic in a type-safe manner.
24
22
 
25
23
  Each form consists of:
26
24
 
27
- - **A schema** — Defined using any validation library that supports [Standard Schema](https://standard-schema.dev/) (Zod, Yup, etc.)
25
+ - **A schema** — Defined using any validation library that supports [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType, etc.)
28
26
  - **Form state** — Managed reactively with full TypeScript support
29
27
  - **Validation** — Automatic validation based on your schema with error messages
30
28
  - **Submission handling** — Built-in submission lifecycle with loading states
29
+ - **Your UI** — NotForm renders nothing—you bring your own components
30
+
31
+ ## Basic Usage
31
32
 
32
- ### Basic Usage
33
+ Import the components and composable you need:
33
34
 
34
35
  ```vue
35
36
  <script setup lang="ts">
@@ -52,7 +53,7 @@ const form = useNotForm({
52
53
  <template>
53
54
  <NotForm
54
55
  :form
55
- @submit="form.submit"
56
+ @submit.prevent="form.submit"
56
57
  @reset="form.reset()"
57
58
  >
58
59
  <NotField
@@ -100,23 +101,104 @@ const form = useNotForm({
100
101
  </template>
101
102
  ```
102
103
 
103
- ### Validation Libraries
104
+ ## Array Fields
105
+
106
+ NotForm includes built-in support for dynamic array fields with add, remove, and reorder operations:
107
+
108
+ ```vue
109
+ <script setup lang="ts">
110
+ import { NotArrayField, NotField, NotForm, NotMessage, useNotForm } from 'notform'
111
+ import { z } from 'zod'
112
+
113
+ const tagSchema = z.string().min(1, 'Tag cannot be empty')
114
+
115
+ const schema = z.object({
116
+ tags: z.array(tagSchema).min(1, 'At least one tag is required'),
117
+ })
118
+
119
+ const form = useNotForm({
120
+ schema,
121
+ })
122
+ </script>
123
+
124
+ <template>
125
+ <NotForm
126
+ :form
127
+ @submit.prevent="form.submit"
128
+ >
129
+ <NotArrayField
130
+ v-slot="{ items, append, remove }"
131
+ path="tags"
132
+ :item-schema="tagSchema"
133
+ >
134
+ <div
135
+ v-for="(item, index) in items"
136
+ :key="item.key"
137
+ >
138
+ <NotField
139
+ v-slot="{ events }"
140
+ :path="item.path"
141
+ >
142
+ <input
143
+ v-bind="events"
144
+ :id="item.path"
145
+ v-model="form.values.tags[index]"
146
+ type="text"
147
+ >
148
+ </NotField>
149
+
150
+ <button
151
+ type="button"
152
+ @click="remove(index)"
153
+ >
154
+ Remove
155
+ </button>
156
+ </div>
157
+
158
+ <button
159
+ type="button"
160
+ @click="append('')"
161
+ >
162
+ Add Tag
163
+ </button>
164
+
165
+ <NotMessage path="tags" />
166
+ </NotArrayField>
167
+ </NotForm>
168
+ </template>
169
+ ```
170
+
171
+ ## Validation Libraries
104
172
 
105
173
  NotForm works with any validation library that implements the Standard Schema interface:
106
174
 
107
175
  - **Zod** — TypeScript-first schema validation
108
- - **Yup** — JavaScript schema builder
109
176
  - **Valibot** — Modular and type-safe schema validation
177
+ - **ArkType** — High-performance runtime type checking
110
178
  - And any other Standard Schema-compatible library
111
179
 
180
+ ## Components and Composables
181
+
182
+ ### Composables
183
+
184
+ - `useNotForm` — Main composable for creating form instances
185
+
186
+ ### Components
187
+
188
+ - `NotForm` — Form wrapper component that provides form context
189
+ - `NotField` — Field component for individual form inputs
190
+ - `NotMessage` — Error message display component
191
+ - `NotArrayField` — Array field component for dynamic form arrays
192
+
112
193
  ## Features
113
194
 
195
+ - **Headless** — Renders nothing. You bring the UI, NotForm brings the logic.
196
+ - **Schema-agnostic** — Works with any Standard Schema validator (Zod, Valibot, ArkType, etc.)
114
197
  - **Type-safe** — Full TypeScript support with inferred types from your schema
115
- - **Composable** — Use with Vue 3 Composition API for flexible form management
198
+ - **Composable** — Built for Vue 3 Composition API with a clean, intuitive API
116
199
  - **Lightweight** — Tiny footprint with tree-shaking support
117
- - **Flexible** — Works with any Standard Schema-compatible validation library
118
200
  - **Array fields** — Built-in support for dynamic array fields with add/remove operations
119
- - **Minimal boilerplate** — Get started quickly with simple, intuitive APIs
201
+ - **Flexible** — Use with any UI library—native HTML, Nuxt UI, Shadcn, or your own components
120
202
 
121
203
  ## Documentation
122
204
 
@@ -126,4 +208,3 @@ For detailed guides, API reference, and examples, visit:
126
208
  ## License
127
209
 
128
210
  [MIT](../../LICENSE) &copy; [Favour Emeka](https://github.com/favorodera)
129
-
package/dist/index.d.ts CHANGED
@@ -215,7 +215,7 @@ type NotFormInstance<TSchema extends ObjectSchema> = Raw<{
215
215
  * </template>
216
216
  * ```
217
217
  */
218
- submit: (event: Event) => Promise<void>;
218
+ submit: (event: SubmitEvent) => Promise<void>;
219
219
  /**
220
220
  * Resets the form to its initial state, or to new values and errors if provided.
221
221
  *
package/dist/index.js CHANGED
@@ -185,12 +185,12 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
185
185
  const form = useNotFormInstance(props.form);
186
186
  const isValidating = ref(false);
187
187
  /** Counter to ensure absolute uniqueness for generated keys. */
188
- let keyCounter = 0;
188
+ const keyCounter = ref(0);
189
189
  /** Stable keys per item that survive reorders, removals, and inserts. */
190
190
  const itemKeys = ref((() => {
191
191
  const initial = getProperty(form.values, props.path);
192
192
  const length = Array.isArray(initial) ? initial.length : 0;
193
- return Array.from({ length }, () => `${props.path}-${keyCounter++}`);
193
+ return Array.from({ length }, () => `${props.path}-${keyCounter.value++}`);
194
194
  })());
195
195
  const validateOn = computed(() => ({
196
196
  onChange: props.validateOn?.onChange ?? form.validateOn.onChange,
@@ -220,7 +220,7 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
220
220
  * @returns A unique key string
221
221
  */
222
222
  function generateKey() {
223
- return `${props.path}-${keyCounter++}`;
223
+ return `${props.path}-${keyCounter.value++}`;
224
224
  }
225
225
  /** Re-aligns itemKeys with current array length (used mostly after external resets) */
226
226
  function syncKeys() {
@@ -259,7 +259,9 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
259
259
  */
260
260
  function append(value) {
261
261
  itemKeys.value.push(generateKey());
262
- mutate((current) => current.push(value));
262
+ mutate((current) => {
263
+ current.push(value);
264
+ });
263
265
  }
264
266
  /**
265
267
  * Prepends an item to the array
@@ -268,7 +270,9 @@ var not_array_field_default = /* @__PURE__ */ defineComponent({
268
270
  function prepend(value) {
269
271
  itemKeys.value.unshift(generateKey());
270
272
  remapArrayFieldState(form, props.path, (previousIndex) => previousIndex + 1);
271
- mutate((current) => current.unshift(value));
273
+ mutate((current) => {
274
+ current.unshift(value);
275
+ });
272
276
  }
273
277
  /**
274
278
  * Removes an item from the array
@@ -400,7 +404,7 @@ var not_field_default = /* @__PURE__ */ defineComponent({
400
404
  const form = useNotFormInstance(props.form);
401
405
  const isValidating = ref(false);
402
406
  /** Timer handle for the current pending debounced validation. */
403
- let debounceTimer;
407
+ const debounceTimer = ref();
404
408
  /** Merges per-field overrides with the form-wide validation config */
405
409
  const validateOn = computed(() => ({
406
410
  ...form.validateOn,
@@ -413,10 +417,9 @@ var not_field_default = /* @__PURE__ */ defineComponent({
413
417
  const isDirty = computed(() => form.dirtyFields.has(props.path));
414
418
  /** Cancels pending debounced validation on blur/unmount to prevent rogue execution. */
415
419
  function clearDebounce() {
416
- if (debounceTimer !== void 0) {
417
- clearTimeout(debounceTimer);
418
- debounceTimer = void 0;
419
- }
420
+ if (debounceTimer.value === void 0) return;
421
+ clearTimeout(debounceTimer.value);
422
+ debounceTimer.value = void 0;
420
423
  }
421
424
  /** Compares current value against baseline to sync dirty state tracking. */
422
425
  function updateDirty() {
@@ -430,7 +433,7 @@ var not_field_default = /* @__PURE__ */ defineComponent({
430
433
  return;
431
434
  }
432
435
  clearDebounce();
433
- debounceTimer = setTimeout(validate, props.debounce);
436
+ debounceTimer.value = setTimeout(validate, props.debounce);
434
437
  }
435
438
  /**
436
439
  * Validates the field and returns the validation result.
@@ -575,9 +578,10 @@ function useNotForm(config) {
575
578
  const isTouched = computed(() => touchedFields.size > 0);
576
579
  const errorsMap = computed(() => {
577
580
  const result = {};
578
- for (const issue of errors) if (issue.path) {
581
+ for (const issue of errors) {
582
+ if (!issue.path) continue;
579
583
  const path = issue.path.map((element) => normalizeSegment(element)).join(".");
580
- if (path && !result[path]) result[path] = issue.message;
584
+ if (path && !Object.hasOwn(result, path)) result[path] = issue.message;
581
585
  }
582
586
  return result;
583
587
  });
@@ -672,7 +676,7 @@ function useNotForm(config) {
672
676
  }
673
677
  /** Clears all error issues from the errors array. */
674
678
  function clearErrors() {
675
- errors.splice(0);
679
+ errors.length = 0;
676
680
  }
677
681
  /**
678
682
  * Retrieves all error issues associated with a specific field path.
@@ -732,21 +736,14 @@ function useNotForm(config) {
732
736
  * @param event The submit event.
733
737
  */
734
738
  async function submit(event) {
739
+ event.preventDefault();
735
740
  isSubmitting.value = true;
736
741
  try {
737
742
  touchAllFields();
738
743
  dirtyAllFields();
739
744
  const result = await validate();
740
- if (result?.issues) {
741
- event.preventDefault();
742
- return;
743
- }
744
- if (config.onSubmit) {
745
- event.preventDefault();
746
- await config.onSubmit(result.value);
747
- }
748
- } catch {
749
- event.preventDefault();
745
+ if (result?.issues) return;
746
+ if (config.onSubmit) await config.onSubmit(result.value);
750
747
  } finally {
751
748
  isSubmitting.value = false;
752
749
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "notform",
3
3
  "type": "module",
4
- "version": "2.2.2",
4
+ "version": "2.2.3",
5
5
  "private": false,
6
6
  "description": "Vue Forms Without the Friction",
7
7
  "author": "Favour Emeka <favorodera@gmail.com>",
@@ -49,18 +49,18 @@
49
49
  "klona": "^2.0.6"
50
50
  },
51
51
  "devDependencies": {
52
- "@favorodera/eslint-config": "^1.0.4",
52
+ "@favorodera/eslint-config": "^1.1.2",
53
53
  "@types/node": "^26.2.0",
54
54
  "@vitejs/plugin-vue": "^6.0.8",
55
- "@vitest/ui": "^4.1.10",
55
+ "@vitest/ui": "^4.1.11",
56
56
  "@vue/test-utils": "^2.4.11",
57
- "happy-dom": "^20.11.2",
57
+ "happy-dom": "^20.11.6",
58
58
  "tsdown": "^0.22.14",
59
59
  "type-fest": "^5.7.0",
60
- "vite": "^8.2.1",
61
- "vitest": "^4.1.10",
60
+ "vite": "^8.2.2",
61
+ "vitest": "^4.1.11",
62
62
  "vue": "^3.0.0",
63
- "vue-tsc": "^3.3.9"
63
+ "vue-tsc": "^3.3.11"
64
64
  },
65
65
  "scripts": {
66
66
  "build": "tsdown",