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 +97 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +21 -24
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<h1>notform</h1>
|
|
3
|
-
<p><strong>
|
|
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
|
|
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
|
-
|
|
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://
|
|
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
|
-
|
|
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
|
-
|
|
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** —
|
|
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
|
-
- **
|
|
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) © [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:
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
417
|
-
|
|
418
|
-
|
|
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)
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
+
"@vitest/ui": "^4.1.11",
|
|
56
56
|
"@vue/test-utils": "^2.4.11",
|
|
57
|
-
"happy-dom": "^20.11.
|
|
57
|
+
"happy-dom": "^20.11.6",
|
|
58
58
|
"tsdown": "^0.22.14",
|
|
59
59
|
"type-fest": "^5.7.0",
|
|
60
|
-
"vite": "^8.2.
|
|
61
|
-
"vitest": "^4.1.
|
|
60
|
+
"vite": "^8.2.2",
|
|
61
|
+
"vitest": "^4.1.11",
|
|
62
62
|
"vue": "^3.0.0",
|
|
63
|
-
"vue-tsc": "^3.3.
|
|
63
|
+
"vue-tsc": "^3.3.11"
|
|
64
64
|
},
|
|
65
65
|
"scripts": {
|
|
66
66
|
"build": "tsdown",
|