nuxt-ui-formwerk 0.1.1

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 ADDED
@@ -0,0 +1,254 @@
1
+ # Nuxt UI Formwerk
2
+
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![License][license-src]][license-href]
6
+ [![Nuxt][nuxt-src]][nuxt-href]
7
+
8
+ Enhanced form components for Nuxt UI with [@formwerk/core](https://formwerk.dev/) integration. This module bridges the gap between Formwerk's powerful form validation and state management with Nuxt UI's beautiful form components.
9
+
10
+ - [✨  Release Notes](/CHANGELOG.md)
11
+
12
+ ## Features
13
+
14
+ - 🎯 **Formwerk Integration** - Seamless integration with [@formwerk/core](https://formwerk.dev/) for advanced form validation
15
+ - 📝 **Enhanced Form Components** - Wraps Nuxt UI components with formwerk capabilities
16
+ - ✅ **Field-level Validation** - Granular validation control with error message handling
17
+ - 🔄 **State Tracking** - Track touched, blurred, and dirty states per field
18
+ - ⚙️ **Flexible Validation Strategies** - Configure when validation occurs (on blur, on input, etc.)
19
+ - 📦 **Auto-import** - Components are automatically available in your app
20
+ - 🎯 **TypeScript** - Full type safety out of the box
21
+
22
+ ## Quick Setup
23
+
24
+ Install the module and its peer dependencies:
25
+
26
+ ```bash
27
+ pnpm add nuxt-ui-formwerk
28
+ ```
29
+
30
+ Add the module to your `nuxt.config.ts`:
31
+
32
+ ```ts
33
+ export default defineNuxtConfig({
34
+ modules: ['@nuxt/ui', 'nuxt-ui-formwerk']
35
+ })
36
+ ```
37
+
38
+ That's it! You can now use enhanced form components in your Nuxt app ✨
39
+
40
+ ## Usage
41
+
42
+ This module provides three main components that wrap Nuxt UI form components with formwerk functionality:
43
+
44
+ ### FormwerkForm
45
+
46
+ The root form component that provides validation context and tracks form state.
47
+
48
+ ```vue
49
+ <script setup lang="ts">
50
+ import { z } from 'zod'
51
+
52
+ const schema = z.object({
53
+ email: z.string().email(),
54
+ password: z.string().min(8)
55
+ })
56
+
57
+ const state = reactive({
58
+ email: '',
59
+ password: ''
60
+ })
61
+ </script>
62
+
63
+ <template>
64
+ <FormwerkForm :schema="schema" :state="state" validate-on="blur" #="{ blurredFields, touchedFields, dirtyFields }">
65
+ <!-- Form content here -->
66
+ <p>Blurred fields: {{ blurredFields.size }}</p>
67
+ </FormwerkForm>
68
+ </template>
69
+ ```
70
+
71
+ #### Props
72
+
73
+ | Prop | Type | Default | Description |
74
+ | ------------ | ------------------------------ | -------- | ----------------------------------------------- |
75
+ | `validateOn` | `'touched' \| 'blur' \| 'dirty'` | `'blur'` | When to trigger validation |
76
+ | `disabled` | `boolean` | `false` | Disable all form fields |
77
+
78
+ #### Slot Props
79
+
80
+ - `blurredFields` - Set of field names that have been blurred
81
+ - `touchedFields` - Set of field names that have been touched
82
+ - `dirtyFields` - Set of field names with modified values
83
+
84
+ ### FormwerkField
85
+
86
+ Enhanced field component that wraps `UFormField` with formwerk validation.
87
+
88
+ ```vue
89
+ <template>
90
+ <FormwerkForm :schema="schema" :state="state">
91
+ <FormwerkField name="email" label="Email" required #="{ setValue, value }">
92
+ <UInput
93
+ :model-value="value"
94
+ @update:model-value="setValue"
95
+ type="email"
96
+ />
97
+ </FormwerkField>
98
+ </FormwerkForm>
99
+ </template>
100
+ ```
101
+
102
+ #### Props
103
+
104
+ Accepts all `UFormField` props except `validateOnInputDelay`, `errorPattern`, `eagerValidation`, and `error` (these are managed by formwerk).
105
+
106
+ #### Slot Props
107
+
108
+ - `setValue` - Function to update field value
109
+ - `value` - Current field value (fieldValue)
110
+
111
+ ### FormwerkGroup
112
+
113
+ Groups related form fields together for nested validation.
114
+
115
+ ```vue
116
+ <template>
117
+ <FormwerkForm :schema="schema" :state="state">
118
+ <FormwerkGroup name="address">
119
+ <FormwerkField name="street" label="Street" #="{ setValue, value }">
120
+ <UInput :model-value="value" @update:model-value="setValue" />
121
+ </FormwerkField>
122
+ <FormwerkField name="city" label="City" #="{ setValue, value }">
123
+ <UInput :model-value="value" @update:model-value="setValue" />
124
+ </FormwerkField>
125
+ </FormwerkGroup>
126
+ </FormwerkForm>
127
+ </template>
128
+ ```
129
+
130
+ #### Props
131
+
132
+ | Prop | Type | Required | Description |
133
+ | ------ | -------- | -------- | ---------------- |
134
+ | `name` | `string` | Yes | Group identifier |
135
+
136
+ ## Complete Example
137
+
138
+ ```vue
139
+ <script setup lang="ts">
140
+ import { z } from 'zod'
141
+
142
+ const schema = z.object({
143
+ name: z.string().min(2, 'Name must be at least 2 characters'),
144
+ email: z.string().email('Invalid email address'),
145
+ password: z.string().min(8, 'Password must be at least 8 characters')
146
+ })
147
+
148
+ const state = reactive({
149
+ name: '',
150
+ email: '',
151
+ password: ''
152
+ })
153
+
154
+ const onSubmit = () => {
155
+ console.log('Form submitted:', state)
156
+ }
157
+ </script>
158
+
159
+ <template>
160
+ <FormwerkForm :schema="schema" :state="state" validate-on="blur" #="{ blurredFields }">
161
+ <div class="space-y-4">
162
+ <FormwerkField name="name" label="Name" required #="{ setValue, value }">
163
+ <UInput :model-value="value" @update:model-value="setValue" />
164
+ </FormwerkField>
165
+
166
+ <FormwerkField name="email" label="Email" required #="{ setValue, value }">
167
+ <UInput :model-value="value" @update:model-value="setValue" type="email" />
168
+ </FormwerkField>
169
+
170
+ <FormwerkField name="password" label="Password" required #="{ setValue, value }">
171
+ <UInput :model-value="value" @update:model-value="setValue" type="password" />
172
+ </FormwerkField>
173
+
174
+ <UButton type="submit" @click="onSubmit">
175
+ Submit
176
+ </UButton>
177
+
178
+ <p class="text-sm text-gray-500">
179
+ Fields blurred: {{ blurredFields.size }}
180
+ </p>
181
+ </div>
182
+ </FormwerkForm>
183
+ </template>
184
+ ```
185
+
186
+ ## Components
187
+
188
+ All components are auto-imported with the `Formwerk` prefix:
189
+
190
+ - `FormwerkForm` - Root form component
191
+ - `FormwerkField` - Field wrapper component
192
+ - `FormwerkGroup` - Field grouping component
193
+
194
+ ## How It Works
195
+
196
+ This module bridges [@formwerk/core](https://formwerk.dev/) with [@nuxt/ui](https://ui.nuxt.com/) by:
197
+
198
+ 1. **FormwerkForm** creates a formwerk form context and manages dual event buses (one for Nuxt UI, one for formwerk)
199
+ 2. **FormwerkField** uses formwerk's `useCustomControl` composable to register fields and handle validation
200
+ 3. Event coordination between both systems ensures validation triggers work as expected
201
+ 4. Field state (touched, blurred, dirty) is tracked and exposed to the parent form
202
+
203
+ The integration allows you to use Nuxt UI's beautiful form components while leveraging formwerk's powerful validation and state management capabilities.
204
+
205
+ ## Contribution
206
+
207
+ <details>
208
+ <summary>Local development</summary>
209
+
210
+ ```bash
211
+ # Install dependencies
212
+ pnpm install
213
+
214
+ # Generate type stubs and prepare playground
215
+ pnpm dev:prepare
216
+
217
+ # Develop with the playground
218
+ pnpm dev
219
+
220
+ # Build the playground
221
+ pnpm dev:build
222
+
223
+ # Run linter (oxlint)
224
+ pnpm lint
225
+
226
+ # Fix linting issues
227
+ pnpm lint:fix
228
+
229
+ # Format code (oxfmt)
230
+ pnpm format
231
+
232
+ # Run tests
233
+ pnpm test
234
+ pnpm test:watch
235
+
236
+ # Type check
237
+ pnpm test:types
238
+
239
+ # Release new version
240
+ pnpm release
241
+ ```
242
+
243
+ </details>
244
+
245
+ <!-- Badges -->
246
+
247
+ [npm-version-src]: https://img.shields.io/npm/v/nuxt-ui-formwerk/latest.svg?style=flat&colorA=020420&colorB=00DC82
248
+ [npm-version-href]: https://npmjs.com/package/nuxt-ui-formwerk
249
+ [npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-ui-formwerk.svg?style=flat&colorA=020420&colorB=00DC82
250
+ [npm-downloads-href]: https://npm.chart.dev/nuxt-ui-formwerk
251
+ [license-src]: https://img.shields.io/npm/l/nuxt-ui-formwerk.svg?style=flat&colorA=020420&colorB=00DC82
252
+ [license-href]: https://npmjs.com/package/nuxt-ui-formwerk
253
+ [nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt.js
254
+ [nuxt-href]: https://nuxt.com
@@ -0,0 +1,8 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ }
5
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
6
+
7
+ export { _default as default };
8
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "nuxt-ui-formwerk",
3
+ "configKey": "uiElements",
4
+ "version": "0.1.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -0,0 +1,23 @@
1
+ import { defineNuxtModule, createResolver, useLogger, hasNuxtModule, addComponentsDir } from '@nuxt/kit';
2
+
3
+ const module$1 = defineNuxtModule({
4
+ meta: {
5
+ name: "nuxt-ui-formwerk",
6
+ configKey: "uiElements"
7
+ },
8
+ defaults: {},
9
+ setup(_options, _nuxt) {
10
+ const resolver = createResolver(import.meta.url);
11
+ const logger = useLogger("nuxt-ui-formwerk");
12
+ if (!hasNuxtModule("@nuxt/ui")) {
13
+ logger.error("[nuxt-ui-formwerk] @nuxt/ui is required. Please install it");
14
+ }
15
+ addComponentsDir({
16
+ path: resolver.resolve("./runtime/components"),
17
+ pathPrefix: false,
18
+ prefix: "Formwerk"
19
+ });
20
+ }
21
+ });
22
+
23
+ export { module$1 as default };
@@ -0,0 +1,18 @@
1
+ import type { FormFieldProps } from "@nuxt/ui";
2
+ type __VLS_Props = Omit<FormFieldProps, "validateOnInputDelay" | "errorPattern" | "eagerValidation" | "error">;
3
+ declare var __VLS_8: {
4
+ setValue: (value: any) => void;
5
+ value: any;
6
+ };
7
+ type __VLS_Slots = {} & {
8
+ default?: (props: typeof __VLS_8) => any;
9
+ };
10
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
14
+ type __VLS_WithSlots<T, S> = T & {
15
+ new (): {
16
+ $slots: S;
17
+ };
18
+ };
@@ -0,0 +1,57 @@
1
+ <script setup>
2
+ import { useCustomControl } from "@formwerk/core";
3
+ import { formwerkOptionsInjectionKey, formwerkBusInjectionKey } from "./Form.vue";
4
+ const props = defineProps({
5
+ as: { type: null, required: false },
6
+ name: { type: String, required: false },
7
+ label: { type: String, required: false },
8
+ description: { type: String, required: false },
9
+ help: { type: String, required: false },
10
+ hint: { type: String, required: false },
11
+ size: { type: null, required: false },
12
+ required: { type: Boolean, required: false },
13
+ class: { type: null, required: false },
14
+ ui: { type: null, required: false }
15
+ });
16
+ const formBus = inject(formBusInjectionKey, void 0);
17
+ const formwerkBus = inject(formwerkBusInjectionKey, void 0);
18
+ const formwerkOptions = inject(formwerkOptionsInjectionKey, void 0);
19
+ const {
20
+ field: { errorMessage, fieldValue, setValue, setBlurred, setTouched, isTouched, isBlurred, isDirty }
21
+ } = useCustomControl({ name: props.name, required: props.required, disabled: formwerkOptions?.value?.disabled });
22
+ const emitFormEvent = (type, name, payload) => {
23
+ if (formwerkBus && name) formwerkBus.emit(type, { name, payload });
24
+ };
25
+ watch(isTouched, (newValue) => emitFormEvent("touched", props.name, newValue));
26
+ watch(isBlurred, (newValue) => emitFormEvent("blur", props.name, newValue));
27
+ watch(isDirty, (newValue) => emitFormEvent("dirty", props.name, newValue));
28
+ const error = computed(() => {
29
+ if (!formwerkOptions || !formwerkOptions.value) return errorMessage.value ? errorMessage.value : void 0;
30
+ switch (formwerkOptions.value.validateOn) {
31
+ case "blur":
32
+ return isBlurred.value && errorMessage.value ? errorMessage.value : void 0;
33
+ default:
34
+ return errorMessage.value ? errorMessage.value : void 0;
35
+ }
36
+ });
37
+ if (formBus) {
38
+ formBus.on(async (event) => {
39
+ switch (event.type) {
40
+ case "blur":
41
+ setBlurred(true);
42
+ break;
43
+ case "change":
44
+ case "input":
45
+ case "focus":
46
+ setTouched(true);
47
+ break;
48
+ }
49
+ });
50
+ }
51
+ </script>
52
+
53
+ <template>
54
+ <UFormField v-bind="props" :error="error">
55
+ <slot :set-value="setValue" :value="fieldValue" />
56
+ </UFormField>
57
+ </template>
@@ -0,0 +1,18 @@
1
+ import type { FormFieldProps } from "@nuxt/ui";
2
+ type __VLS_Props = Omit<FormFieldProps, "validateOnInputDelay" | "errorPattern" | "eagerValidation" | "error">;
3
+ declare var __VLS_8: {
4
+ setValue: (value: any) => void;
5
+ value: any;
6
+ };
7
+ type __VLS_Slots = {} & {
8
+ default?: (props: typeof __VLS_8) => any;
9
+ };
10
+ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
14
+ type __VLS_WithSlots<T, S> = T & {
15
+ new (): {
16
+ $slots: S;
17
+ };
18
+ };
File without changes
@@ -0,0 +1,60 @@
1
+ <script>
2
+ import { useFormContext } from "@formwerk/core";
3
+ export const formwerkOptionsInjectionKey = /* @__PURE__ */ Symbol("nuxt-ui-formwerk.form-options");
4
+ export const formwerkBusInjectionKey = /* @__PURE__ */ Symbol("nuxt-ui-formwerk.form-events");
5
+ </script>
6
+
7
+ <script setup>
8
+ const { context } = useFormContext();
9
+ const { validateOn = "blur", disabled = false } = defineProps({
10
+ validateOn: { type: String, required: false },
11
+ disabled: { type: Boolean, required: false }
12
+ });
13
+ const formwerkBus = useEventBus(`formwerk-form-${context.id}`);
14
+ const NuxtUiFormBus = useEventBus(`form-${context.id}`);
15
+ const dirtyFields = reactive(/* @__PURE__ */ new Set());
16
+ const touchedFields = reactive(/* @__PURE__ */ new Set());
17
+ const blurredFields = reactive(/* @__PURE__ */ new Set());
18
+ provide(formwerkBusInjectionKey, formwerkBus);
19
+ provide(formBusInjectionKey, NuxtUiFormBus);
20
+ provide(
21
+ formwerkOptionsInjectionKey,
22
+ computed(() => ({
23
+ validateOn
24
+ }))
25
+ );
26
+ provide(
27
+ formOptionsInjectionKey,
28
+ computed(() => ({
29
+ disabled
30
+ }))
31
+ );
32
+ const toggleState = (set, payload) => {
33
+ if (!payload) return;
34
+ const { name, payload: isSet } = payload;
35
+ if (isSet) {
36
+ set.add(name);
37
+ } else {
38
+ set.delete(name);
39
+ }
40
+ };
41
+ formwerkBus.on(async (event, payload) => {
42
+ switch (event) {
43
+ case "touched":
44
+ toggleState(touchedFields, payload);
45
+ break;
46
+ case "blur":
47
+ toggleState(blurredFields, payload);
48
+ break;
49
+ case "dirty":
50
+ toggleState(dirtyFields, payload);
51
+ break;
52
+ }
53
+ });
54
+ </script>
55
+
56
+ <template>
57
+ <div>
58
+ <slot :blurred-fields="blurredFields" :touched-fields="touchedFields" :dirty-fields="dirtyFields" />
59
+ </div>
60
+ </template>
File without changes
@@ -0,0 +1,16 @@
1
+ interface Props {
2
+ name: string;
3
+ }
4
+ declare var __VLS_1: {};
5
+ type __VLS_Slots = {} & {
6
+ default?: (props: typeof __VLS_1) => any;
7
+ };
8
+ declare const __VLS_base: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
12
+ type __VLS_WithSlots<T, S> = T & {
13
+ new (): {
14
+ $slots: S;
15
+ };
16
+ };
@@ -0,0 +1,15 @@
1
+ <script setup>
2
+ import { useFormGroup } from "@formwerk/core";
3
+ defineProps({
4
+ name: { type: String, required: true }
5
+ });
6
+ useFormGroup({
7
+ name: "options"
8
+ });
9
+ </script>
10
+
11
+ <template>
12
+ <div>
13
+ <slot />
14
+ </div>
15
+ </template>
@@ -0,0 +1,16 @@
1
+ interface Props {
2
+ name: string;
3
+ }
4
+ declare var __VLS_1: {};
5
+ type __VLS_Slots = {} & {
6
+ default?: (props: typeof __VLS_1) => any;
7
+ };
8
+ declare const __VLS_base: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
12
+ type __VLS_WithSlots<T, S> = T & {
13
+ new (): {
14
+ $slots: S;
15
+ };
16
+ };
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "nuxt-ui-formwerk",
3
+ "version": "0.1.1",
4
+ "description": "A collection of beautiful, animated UI components for Nuxt applications",
5
+ "license": "MIT",
6
+ "repository": "https://github.com/genu/nuxt-ui-formwerk.git",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "type": "module",
11
+ "main": "./dist/module.mjs",
12
+ "typesVersions": {
13
+ "*": {
14
+ ".": [
15
+ "./dist/types.d.mts"
16
+ ]
17
+ }
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/types.d.mts",
22
+ "import": "./dist/module.mjs"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "@nuxt/kit": "^4.2.2"
27
+ },
28
+ "devDependencies": {
29
+ "@nuxt/devtools": "^3.1.1",
30
+ "@nuxt/module-builder": "^1.0.2",
31
+ "@nuxt/schema": "^4.2.2",
32
+ "@nuxt/test-utils": "^3.21.0",
33
+ "@types/culori": "^4.0.1",
34
+ "@types/node": "latest",
35
+ "changelogen": "^0.6.2",
36
+ "nuxt": "^4.2.2",
37
+ "oxfmt": "^0.20.0",
38
+ "oxlint": "^1.35.0",
39
+ "oxlint-tsgolint": "^0.10.0",
40
+ "typescript": "~5.9.3",
41
+ "vitest": "^4.0.16",
42
+ "vue-tsc": "^3.2.1"
43
+ },
44
+ "peerDependencies": {
45
+ "@nuxt/ui": "^4.0.0",
46
+ "@formwerk/core": "^0.14.4"
47
+ },
48
+ "scripts": {
49
+ "dev": "pnpm dev:prepare && nuxi dev playground",
50
+ "dev:build": "nuxi build playground",
51
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
52
+ "lint": "oxlint --type-aware",
53
+ "lint:fix": "oxlint --fix --type-aware",
54
+ "format": "oxfmt --write",
55
+ "format:check": "oxfmt --check",
56
+ "release": "pnpm lint && pnpm test && pnpm prepack && changelogen --release && pnpm publish && git push --follow-tags",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest watch",
59
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
60
+ }
61
+ }