@softwareproduction/nuxt-introspection-forms 0.1.0

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,344 @@
1
+ # @softwareproduction/nuxt-introspection-forms
2
+
3
+ Nuxt module that wraps [@softwareproduction/introspection-forms](../introspection-forms) for seamless integration with Nuxt 3 and 4 applications. It handles component registration, composable auto-imports, alias configuration, and global defaults injection — so you can use schema-driven forms with zero boilerplate.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Module Setup](#module-setup)
9
+ - [Component Mapping Plugin](#component-mapping-plugin)
10
+ - [Usage in Pages & Components](#usage-in-pages--components)
11
+ - [GraphQL Codegen Integration](#graphql-codegen-integration)
12
+ - [Module Options](#module-options)
13
+ - [Provided Aliases](#provided-aliases)
14
+ - [Auto-Imports](#auto-imports)
15
+ - [Validation with Dryv](#validation-with-dryv)
16
+ - [Advanced Configuration](#advanced-configuration)
17
+ - [License](#license)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ yarn add @softwareproduction/nuxt-introspection-forms @softwareproduction/introspection-forms
23
+ yarn add -D @graphql-codegen/cli graphql
24
+ ```
25
+
26
+ Peer dependencies: `vue >= 3.4`, `nuxt >= 3.9`.
27
+
28
+ ## Module Setup
29
+
30
+ Add the module to your `nuxt.config.ts`:
31
+
32
+ ```ts
33
+ export default defineNuxtConfig({
34
+ modules: ['@softwareproduction/nuxt-introspection-forms'],
35
+
36
+ introspectionForms: {
37
+ // Path to generated introspection types (relative to rootDir)
38
+ generatedPath: 'types/generated/introspection',
39
+ },
40
+ })
41
+ ```
42
+
43
+ The module will:
44
+
45
+ 1. Register `<IntrospectionForm>` and `<IntrospectionField>` as global components.
46
+ 2. Auto-import `useIntrospectionForm` and `useIntrospectionFormsEnumFilter` composables.
47
+ 3. Create `#introspection-types` and `#introspection-forms` aliases for convenient imports.
48
+ 4. Register a client-side plugin that provides global defaults via `inject`.
49
+
50
+ ## Component Mapping Plugin
51
+
52
+ Create a Nuxt plugin to define which UI components render which field types. This is where you connect `@softwareproduction/introspection-forms` to your design system.
53
+
54
+ ```ts
55
+ // app/plugins/configure-forms.ts
56
+ import {
57
+ FormInput,
58
+ FormCheckbox,
59
+ FormRadio,
60
+ FormSelect,
61
+ FormTextarea,
62
+ FormDateInput,
63
+ } from '#components'
64
+ import type { IntrospectionFormsDefaults, IntrospectionField } from '@softwareproduction/introspection-forms'
65
+ import { withProps, useIntrospectionFormsEnumFilter } from '@softwareproduction/introspection-forms'
66
+
67
+ export default defineNuxtPlugin(nuxtApp => {
68
+ const { filterEnumValues } = useIntrospectionFormsEnumFilter()
69
+
70
+ nuxtApp.provide('introspection-forms', {
71
+ // Map by TypeScript scalar type
72
+ byFieldType: {
73
+ string: { component: FormInput },
74
+ number: { component: FormInput, props: { type: 'number' } },
75
+ boolean: { component: FormCheckbox },
76
+ enum: (introspection: IntrospectionField) => ({
77
+ component: introspection.enumValues.length > 5 ? FormSelect : FormRadio,
78
+ props: withProps<typeof FormRadio>(() => ({
79
+ options: (_, t) =>
80
+ filterEnumValues(introspection).map(value => ({
81
+ value,
82
+ label: t(`forms.${introspection.originalType}.${value}`),
83
+ })),
84
+ })),
85
+ }),
86
+ },
87
+
88
+ // Map by original GraphQL type name
89
+ byOriginalType: {
90
+ DateTime: { component: FormDateInput },
91
+ Float: { component: FormInput, props: { type: 'number', step: '0.01' } },
92
+ },
93
+
94
+ // Map by field name (regex matching, first match wins)
95
+ byFieldName: [
96
+ { regexp: /^email/i, config: { component: FormInput, props: { type: 'email' } } },
97
+ { regexp: /phone|telefon/i, config: { component: FormInput, props: { type: 'tel' } } },
98
+ { regexp: /^(body|message|notes)/i, config: { component: FormTextarea } },
99
+ { regexp: /files/i, config: { component: FormInput, props: { type: 'file', multiple: true } } },
100
+ { regexp: /^(password|passwort)/i, config: { component: FormInput, props: { type: 'password' } } },
101
+ ],
102
+
103
+ // Global enum value filters
104
+ enumFilters: {
105
+ Salutation: values => values.filter(v => v !== 'None'),
106
+ },
107
+ } satisfies IntrospectionFormsDefaults)
108
+ })
109
+ ```
110
+
111
+ ## Usage in Pages & Components
112
+
113
+ The module auto-imports everything you need. Import only the generated introspection type:
114
+
115
+ ```vue
116
+ <script setup lang="ts">
117
+ import { TypeOfContactFormInput } from '#introspection-types'
118
+
119
+ const data = ref(TypeOfContactFormInput.create())
120
+
121
+ const form = useIntrospectionForm(TypeOfContactFormInput, {
122
+ firstName: true,
123
+ lastName: true,
124
+ email: true,
125
+ body: true,
126
+ })
127
+
128
+ async function submit() {
129
+ // data.value now contains the form values
130
+ await $fetch('/api/contact', { method: 'POST', body: data.value })
131
+ }
132
+ </script>
133
+
134
+ <template>
135
+ <IntrospectionForm :form="form" :model="data">
136
+ <button @click.prevent="submit">Send</button>
137
+ </IntrospectionForm>
138
+ </template>
139
+ ```
140
+
141
+ ### With Field Overrides
142
+
143
+ Override defaults for specific fields without losing the auto-resolved component:
144
+
145
+ ```vue
146
+ <script setup lang="ts">
147
+ import { TypeOfRegistrationFormInput } from '#introspection-types'
148
+
149
+ const data = ref(TypeOfRegistrationFormInput.create({ useSameAddress: true }))
150
+
151
+ const form = useIntrospectionForm(TypeOfRegistrationFormInput, {
152
+ firstName: { span: 1 }, // Keep default component, adjust layout
153
+ lastName: { span: 1 },
154
+ email: true, // Use defaults entirely
155
+ phone: {
156
+ visible: model => model.preferredContact === 'Phone', // Conditional visibility
157
+ },
158
+ acceptTerms: { span: 2 },
159
+ })
160
+ </script>
161
+ ```
162
+
163
+ ### With Nested Forms
164
+
165
+ ```vue
166
+ <script setup lang="ts">
167
+ import { TypeOfOrderInput, TypeOfAddressInput } from '#introspection-types'
168
+
169
+ const data = ref(TypeOfOrderInput.create())
170
+
171
+ const addressForm = useIntrospectionForm(TypeOfAddressInput, {
172
+ street: true,
173
+ houseNumber: true,
174
+ zipCode: true,
175
+ city: true,
176
+ })
177
+
178
+ const form = useIntrospectionForm(TypeOfOrderInput, {
179
+ productId: true,
180
+ quantity: true,
181
+ shippingAddress: { form: addressForm },
182
+ })
183
+ </script>
184
+ ```
185
+
186
+ ## GraphQL Codegen Integration
187
+
188
+ Set up the codegen plugin to generate introspection metadata from your schema:
189
+
190
+ ```ts
191
+ // codegen.config.ts
192
+ import type { CodegenConfig } from '@graphql-codegen/cli'
193
+
194
+ const config: CodegenConfig = {
195
+ schema: './schema.graphql',
196
+ generates: {
197
+ './types/generated/introspection/_placeholder.ts': {
198
+ plugins: ['@softwareproduction/introspection-forms/codegen'],
199
+ config: {
200
+ output: './types/generated/introspection',
201
+ typesImport: '../graphql-types',
202
+ introspectionTypeImport: '@softwareproduction/introspection-forms',
203
+ },
204
+ },
205
+ },
206
+ }
207
+
208
+ export default config
209
+ ```
210
+
211
+ Add scripts to your `package.json`:
212
+
213
+ ```json
214
+ {
215
+ "scripts": {
216
+ "gen:forms": "graphql-codegen --config codegen.config.ts",
217
+ "gen:types": "graphql-codegen --config codegen-types.config.ts"
218
+ }
219
+ }
220
+ ```
221
+
222
+ Run both generators when your schema changes:
223
+
224
+ ```bash
225
+ yarn gen:types && yarn gen:forms
226
+ ```
227
+
228
+ ## Module Options
229
+
230
+ Configure via `introspectionForms` in `nuxt.config.ts`:
231
+
232
+ | Option | Type | Default | Description |
233
+ |--------|------|---------|-------------|
234
+ | `generatedPath` | `string` | `'types/generated/introspection'` | Path to generated introspection files (relative to `rootDir`) |
235
+ | `defaults` | `IntrospectionFormsDefaults` | `undefined` | Static component mapping defaults (alternative to the plugin approach for simple configs) |
236
+
237
+ ## Provided Aliases
238
+
239
+ The module registers two aliases for convenient imports:
240
+
241
+ | Alias | Resolves To | Usage |
242
+ |-------|-------------|-------|
243
+ | `#introspection-types` | `<rootDir>/<generatedPath>` | `import { TypeOfXxx } from '#introspection-types'` |
244
+ | `#introspection-forms` | `@softwareproduction/introspection-forms` package | `import { withProps } from '#introspection-forms'` |
245
+
246
+ ## Auto-Imports
247
+
248
+ These are available globally without manual imports:
249
+
250
+ | Import | Source | Description |
251
+ |--------|--------|-------------|
252
+ | `useIntrospectionForm` | `@softwareproduction/introspection-forms` | Create a form runtime |
253
+ | `useIntrospectionFormsEnumFilter` | `@softwareproduction/introspection-forms` | Access enum filters |
254
+
255
+ Components registered globally:
256
+
257
+ | Component | Description |
258
+ |-----------|-------------|
259
+ | `<IntrospectionForm>` | Top-level form renderer |
260
+ | `<IntrospectionField>` | Individual field renderer |
261
+
262
+ ## Validation with Dryv
263
+
264
+ The module integrates with [Dryv](https://github.com/mhusseini/dryvjs) for client-side validation. Pass a `DryvValidationRuleSet` to `useIntrospectionForm` and use `v-model:validate` to receive the validate command from the component:
265
+
266
+ ```vue
267
+ <script setup lang="ts">
268
+ import { TypeOfContactFormInput } from '#introspection-types'
269
+ import { ContactFormValidationSet } from '#validation'
270
+
271
+ const data = ref(TypeOfContactFormInput.create())
272
+ const validate = ref<(checkOnly?: boolean) => Promise<boolean>>()
273
+
274
+ const form = useIntrospectionForm(
275
+ TypeOfContactFormInput,
276
+ ContactFormValidationSet,
277
+ {
278
+ firstName: true,
279
+ lastName: true,
280
+ email: true,
281
+ },
282
+ )
283
+
284
+ async function submit() {
285
+ const success = await validate.value?.()
286
+ if (!success) return
287
+ await $fetch('/api/submit', { method: 'POST', body: data.value })
288
+ }
289
+ </script>
290
+
291
+ <template>
292
+ <IntrospectionForm :form="form" :model="data" v-model:validate="validate">
293
+ <button @click.prevent="submit">Submit</button>
294
+ </IntrospectionForm>
295
+ </template>
296
+ ```
297
+
298
+ The component internally creates a Dryv validation session when rules are provided and exposes the `validate` function via `v-model:validate`. Validation errors appear inline beneath each field automatically.
299
+
300
+ ## Advanced Configuration
301
+
302
+ ### Multiple Form Configs (Spread Pattern)
303
+
304
+ Break large forms into logical groups:
305
+
306
+ ```ts
307
+ const personalFields = { firstName: true, lastName: true, dateOfBirth: true }
308
+ const contactFields = { email: true, phone: true, preferredContact: true }
309
+ const consentFields = { acceptTerms: { span: 2 }, acceptNewsletter: { span: 2 } }
310
+
311
+ const form = useIntrospectionForm(TypeOfRegistrationFormInput, {
312
+ ...personalFields,
313
+ ...contactFields,
314
+ ...consentFields,
315
+ })
316
+ ```
317
+
318
+ ### Form-Level Visibility and Disabled
319
+
320
+ ```ts
321
+ const form = useIntrospectionForm({
322
+ introspection: TypeOfPaymentInput,
323
+ config: { cardNumber: true, cvv: true, expiryDate: true },
324
+ visible: model => model.paymentMethod === 'creditCard',
325
+ disabled: model => model.isProcessing,
326
+ })
327
+ ```
328
+
329
+ ### Storage Persistence per Form
330
+
331
+ ```vue
332
+ <IntrospectionForm
333
+ :form="form"
334
+ :model="data"
335
+ storage="session"
336
+ :interceptStorage="(stored) => ({ ...stored, acceptTerms: false })"
337
+ >
338
+ ...
339
+ </IntrospectionForm>
340
+ ```
341
+
342
+ ## License
343
+
344
+ MIT
@@ -0,0 +1,5 @@
1
+ module.exports = function(...args) {
2
+ return import('./module.mjs').then(m => m.default.call(this, ...args))
3
+ }
4
+ const _meta = module.exports.meta = require('./module.json')
5
+ module.exports.getMeta = () => Promise.resolve(_meta)
@@ -0,0 +1,30 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /**
5
+ * The folder where the generated introspection types are located (relative to rootDir).
6
+ * @default 'types/generated/introspection'
7
+ */
8
+ generatedPath?: string;
9
+ /**
10
+ * Component mapping configuration for automatic field resolution.
11
+ * Configure which Vue components render which field types.
12
+ */
13
+ defaults?: {
14
+ /** Map field TypeScript types (string, number, boolean, enum) to component configs. */
15
+ byFieldType?: Record<string, unknown>;
16
+ /** Map original GraphQL types (DateTime, etc.) to component configs. */
17
+ byOriginalType?: Record<string, unknown>;
18
+ /** Match field names by regex to component configs. */
19
+ byFieldName?: {
20
+ regexp: RegExp;
21
+ config: unknown;
22
+ }[];
23
+ /** Filter enum values by original type name. */
24
+ enumFilters?: Record<string, (values: string[]) => string[]>;
25
+ };
26
+ }
27
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
28
+
29
+ export { _default as default };
30
+ export type { ModuleOptions };
@@ -0,0 +1,30 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /**
5
+ * The folder where the generated introspection types are located (relative to rootDir).
6
+ * @default 'types/generated/introspection'
7
+ */
8
+ generatedPath?: string;
9
+ /**
10
+ * Component mapping configuration for automatic field resolution.
11
+ * Configure which Vue components render which field types.
12
+ */
13
+ defaults?: {
14
+ /** Map field TypeScript types (string, number, boolean, enum) to component configs. */
15
+ byFieldType?: Record<string, unknown>;
16
+ /** Map original GraphQL types (DateTime, etc.) to component configs. */
17
+ byOriginalType?: Record<string, unknown>;
18
+ /** Match field names by regex to component configs. */
19
+ byFieldName?: {
20
+ regexp: RegExp;
21
+ config: unknown;
22
+ }[];
23
+ /** Filter enum values by original type name. */
24
+ enumFilters?: Record<string, (values: string[]) => string[]>;
25
+ };
26
+ }
27
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
28
+
29
+ export { _default as default };
30
+ export type { ModuleOptions };
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "@softwareproduction/nuxt-introspection-forms",
3
+ "configKey": "introspectionForms",
4
+ "compatibility": {
5
+ "nuxt": "^3.16.0 || ^4.0.0"
6
+ },
7
+ "version": "0.1.0",
8
+ "builder": {
9
+ "@nuxt/module-builder": "0.8.4",
10
+ "unbuild": "2.0.0"
11
+ }
12
+ }
@@ -0,0 +1,49 @@
1
+ import { defineNuxtModule, createResolver, addComponent, addImportsDir, addPlugin } from '@nuxt/kit';
2
+ import { join } from 'path';
3
+
4
+ const module = defineNuxtModule({
5
+ meta: {
6
+ name: "@softwareproduction/nuxt-introspection-forms",
7
+ configKey: "introspectionForms",
8
+ compatibility: {
9
+ nuxt: "^3.16.0 || ^4.0.0"
10
+ }
11
+ },
12
+ defaults: {
13
+ generatedPath: "types/generated/introspection"
14
+ },
15
+ setup(options, nuxt) {
16
+ const resolver = createResolver(import.meta.url);
17
+ addComponent({
18
+ name: "IntrospectionForm",
19
+ filePath: "@softwareproduction/introspection-forms",
20
+ export: "IntrospectionForm"
21
+ });
22
+ addComponent({
23
+ name: "IntrospectionField",
24
+ filePath: "@softwareproduction/introspection-forms",
25
+ export: "IntrospectionField"
26
+ });
27
+ nuxt.options.alias["#introspection-types"] = join(nuxt.options.rootDir, options.generatedPath);
28
+ nuxt.options.alias["#introspection-forms"] = "@softwareproduction/introspection-forms";
29
+ addImportsDir(resolver.resolve("./runtime/composables"));
30
+ addPlugin({
31
+ src: resolver.resolve("./runtime/plugins/introspection-forms.client"),
32
+ mode: "all"
33
+ });
34
+ nuxt.options.runtimeConfig.public = nuxt.options.runtimeConfig.public || {};
35
+ nuxt.options.runtimeConfig.__introspectionFormsDefaults = options.defaults ? JSON.parse(JSON.stringify(options.defaults, (_key, value) => {
36
+ if (value instanceof RegExp)
37
+ return `__REGEXP__${value.source}__FLAGS__${value.flags}`;
38
+ if (typeof value === "function")
39
+ return `__FUNCTION__${value.toString()}`;
40
+ return value;
41
+ })) : void 0;
42
+ nuxt.options.appConfig = nuxt.options.appConfig || {};
43
+ nuxt.options.appConfig.introspectionForms = {
44
+ hasDefaults: !!options.defaults
45
+ };
46
+ }
47
+ });
48
+
49
+ export { module as default };
File without changes
@@ -0,0 +1,2 @@
1
+ export { useIntrospectionForm } from "./useIntrospectionForm.js";
2
+ export { useIntrospectionFormsEnumFilter } from "./useIntrospectionFormsEnumFilter.js";
@@ -0,0 +1 @@
1
+ export { useIntrospectionForm } from "@softwareproduction/introspection-forms";
@@ -0,0 +1 @@
1
+ export { useIntrospectionFormsEnumFilter } from "@softwareproduction/introspection-forms";
@@ -0,0 +1,11 @@
1
+ import { defineNuxtPlugin } from "#app";
2
+ import { INTROSPECTION_FORMS_KEY } from "@softwareproduction/introspection-forms";
3
+ export default defineNuxtPlugin({
4
+ name: "introspection-forms",
5
+ setup(nuxtApp) {
6
+ const existingDefaults = nuxtApp.$introspectionForms;
7
+ if (existingDefaults) {
8
+ nuxtApp.vueApp.provide(INTROSPECTION_FORMS_KEY, existingDefaults);
9
+ }
10
+ }
11
+ });
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.js'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.js'
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module'
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@softwareproduction/nuxt-introspection-forms",
3
+ "version": "0.1.0",
4
+ "description": "Nuxt module for introspection-forms — schema-driven form generation from GraphQL",
5
+ "author": "Munir Husseini (https://github.com/mhusseini)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mhusseini/introspection-forms.git",
10
+ "directory": "packages/nuxt-introspection-forms"
11
+ },
12
+ "type": "module",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/types.d.ts",
16
+ "import": "./dist/module.mjs"
17
+ }
18
+ },
19
+ "main": "./dist/module.mjs",
20
+ "types": "./dist/types.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "nuxt-module-build build",
26
+ "dev": "nuxt-module-build build --watch",
27
+ "typecheck": "tsc --noEmit"
28
+ },
29
+ "dependencies": {
30
+ "@nuxt/kit": "^3.16.0",
31
+ "@softwareproduction/introspection-forms": "0.1.0"
32
+ },
33
+ "peerDependencies": {
34
+ "nuxt": "^3.16.0 || ^4.0.0",
35
+ "vue": "^3.4.0"
36
+ },
37
+ "devDependencies": {
38
+ "@nuxt/module-builder": "^0.8.0",
39
+ "nuxt": "^3.16.0",
40
+ "typescript": "^5.7.0",
41
+ "vue": "^3.5.0"
42
+ }
43
+ }