@ederzeel/nuxt-schema-form-nightly 0.1.0-28989865.c565459
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/.github/semantic.yml +15 -0
- package/.github/workflows/ci.yml +79 -0
- package/.github/workflows/release.yml +62 -0
- package/LICENSE.md +9 -0
- package/README.md +11 -0
- package/eslint.config.mjs +73 -0
- package/package.json +66 -0
- package/playground/app.vue +5 -0
- package/playground/assets/schema-custom.json +111 -0
- package/playground/assets/schema-easy.json +16 -0
- package/playground/assets/schema.json +116 -0
- package/playground/components/ColorMode.vue +26 -0
- package/playground/components/CustomInput.vue +7 -0
- package/playground/components/MultiStage.vue +164 -0
- package/playground/components/Stepper.vue +63 -0
- package/playground/nuxt.config.ts +13 -0
- package/playground/package.json +8 -0
- package/playground/pages/index.vue +32 -0
- package/playground/tailwind.config.js +1 -0
- package/playground/tsconfig.json +4 -0
- package/pnpm-workspace.yaml +2 -0
- package/prettier.config.mjs +8 -0
- package/scripts/bump-nightly.ts +25 -0
- package/scripts/release-nightly.sh +24 -0
- package/scripts/release.sh +21 -0
- package/src/defaults.ts +3 -0
- package/src/module.ts +35 -0
- package/src/plugins/app-config.ts +24 -0
- package/src/plugins/components.ts +68 -0
- package/src/plugins/nuxt-environment.ts +40 -0
- package/src/runtime/components/SComponent.vue +57 -0
- package/src/runtime/components/SForm.vue +59 -0
- package/src/runtime/components/SInputField.vue +25 -0
- package/src/runtime/components/SObject.vue +34 -0
- package/src/runtime/components/SSelect.vue +26 -0
- package/src/runtime/components/STextarea.vue +24 -0
- package/src/runtime/components/SToggle.vue +21 -0
- package/src/runtime/types/utils.ts +44 -0
- package/src/unplugin.ts +46 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
// import { SComponent } from 'nuxt-schema-form'
|
|
3
|
+
import {
|
|
4
|
+
StepperDescription,
|
|
5
|
+
StepperIndicator,
|
|
6
|
+
StepperItem,
|
|
7
|
+
StepperRoot,
|
|
8
|
+
StepperSeparator,
|
|
9
|
+
StepperTitle,
|
|
10
|
+
StepperTrigger
|
|
11
|
+
} from 'reka-ui'
|
|
12
|
+
|
|
13
|
+
export interface FormError<T extends string = string> {
|
|
14
|
+
path: T
|
|
15
|
+
message: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
id: string
|
|
20
|
+
title: string
|
|
21
|
+
jsonSchemaPath: string
|
|
22
|
+
validateFields: (fields: string[]) => Promise<boolean>
|
|
23
|
+
properties: PropertiesType
|
|
24
|
+
modelValue: Record<string, unknown>
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
type PropertiesType = {
|
|
28
|
+
properties: {
|
|
29
|
+
title: string
|
|
30
|
+
description: string
|
|
31
|
+
properties: PropertiesType
|
|
32
|
+
icon: PropertiesType
|
|
33
|
+
[key: string]: unknown
|
|
34
|
+
}
|
|
35
|
+
[key: string]: unknown
|
|
36
|
+
}
|
|
37
|
+
const emit = defineEmits(['update:modelValue'])
|
|
38
|
+
const onInput = (value: unknown, key: string) => {
|
|
39
|
+
emit('update:modelValue', { ...props.modelValue, [key]: value })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const stage = ref(0)
|
|
43
|
+
const children = computed(() => Object.entries(props.properties).map((x, i) => [x[0], x[1], i] as const))
|
|
44
|
+
|
|
45
|
+
type ChildIdsKeys = {
|
|
46
|
+
properties?: ChildIdsKeys
|
|
47
|
+
}
|
|
48
|
+
const getChildFieldIds = (id: string, schema: ChildIdsKeys) => {
|
|
49
|
+
const children: string[] = []
|
|
50
|
+
if (schema.properties == null) return children
|
|
51
|
+
for (const [key, child] of Object.entries(schema.properties)) {
|
|
52
|
+
if (child.properties != null) {
|
|
53
|
+
const childIds = getChildFieldIds(key, child)
|
|
54
|
+
childIds.map(x => `${id}.${x}`)
|
|
55
|
+
children.push(...childIds)
|
|
56
|
+
} else {
|
|
57
|
+
children.push(`${id}.${key}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return children
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
|
|
65
|
+
|
|
66
|
+
const steps = computed(() =>
|
|
67
|
+
children.value.map((x, i) => ({
|
|
68
|
+
step: i,
|
|
69
|
+
title: x[1].title,
|
|
70
|
+
description: x[1].description,
|
|
71
|
+
icon: x[1].icon || '',
|
|
72
|
+
error: formErrors?.value.find(error => error.path.includes(`${x[0]}`))
|
|
73
|
+
}))
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
const onNext = async () => {
|
|
77
|
+
const child = children.value[stage.value]
|
|
78
|
+
console.log(await props.validateFields(getChildFieldIds(child[0], child[1])))
|
|
79
|
+
if (!(await props.validateFields(getChildFieldIds(child[0], child[1])))) return
|
|
80
|
+
stage.value++
|
|
81
|
+
}
|
|
82
|
+
</script>
|
|
83
|
+
|
|
84
|
+
<template>
|
|
85
|
+
<div class="stepper-container">
|
|
86
|
+
<!-- {{ formErrors }} -->
|
|
87
|
+
<!-- {{ `${children[stage][0]}` }} -->
|
|
88
|
+
<!-- {{ formErrors?.find((error) => error.path.includes(`${children[stage][0]}`)) }} -->
|
|
89
|
+
<h1>{{ props.title }}</h1>
|
|
90
|
+
|
|
91
|
+
<div class="stepper">
|
|
92
|
+
<StepperRoot v-model="stage" class="flex gap-2 w-full">
|
|
93
|
+
<StepperItem
|
|
94
|
+
v-for="item in steps"
|
|
95
|
+
:key="item.step"
|
|
96
|
+
class="w-full flex justify-center gap-2 cursor-pointer group relative px-4"
|
|
97
|
+
:data-error="item.error ? 'error' : undefined"
|
|
98
|
+
:step="item.step"
|
|
99
|
+
>
|
|
100
|
+
<StepperTrigger
|
|
101
|
+
class="inline-flex border-2 shadow-sm items-center text-white bg-primary-500 border-primary-500 group-data-[error=error]:bg-red-500 group-data-[error=error]:border-red-500 group-data-[state=inactive]:border-gray-200 group-data-[state=inactive]:bg-white group-data-[state=inactive]:text-stone-700 group-data-[disabled]:opacity-50 group-data-[disabled]:cursor-not-allowed justify-center rounded-full w-10 h-10 shrink-0 focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none"
|
|
102
|
+
>
|
|
103
|
+
<StepperIndicator>
|
|
104
|
+
<UIcon :name="item.icon" class="w-5 h-5 ms-auto transform transition-transform duration-200" />
|
|
105
|
+
</StepperIndicator>
|
|
106
|
+
</StepperTrigger>
|
|
107
|
+
|
|
108
|
+
<StepperSeparator
|
|
109
|
+
v-if="item.step !== steps[steps.length - 1].step"
|
|
110
|
+
class="absolute block top-5 left-[calc(50%+30px)] right-[calc(-50%+20px)] h-0.5 rounded-full bg-stone-300/50 shrink-0"
|
|
111
|
+
/>
|
|
112
|
+
|
|
113
|
+
<div
|
|
114
|
+
class="absolute text-center top-full left-0 w-full mt-2 text-stone-700 dark:text-white group-data-[disabled]:opacity-50"
|
|
115
|
+
>
|
|
116
|
+
<StepperTitle class="font-medium">
|
|
117
|
+
{{ item.title }}
|
|
118
|
+
</StepperTitle>
|
|
119
|
+
<StepperDescription class="hidden sm:block text-xs">
|
|
120
|
+
{{ item.description }}
|
|
121
|
+
</StepperDescription>
|
|
122
|
+
</div>
|
|
123
|
+
</StepperItem>
|
|
124
|
+
</StepperRoot>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="stepper-content">
|
|
127
|
+
<section v-for="[key, options, index] of children" v-show="stage == index" :key="key">
|
|
128
|
+
<SComponent
|
|
129
|
+
:id="id.length <= 0 ? `${key}` : `${id}.${key}`"
|
|
130
|
+
:key="key"
|
|
131
|
+
:model-value="modelValue[key]"
|
|
132
|
+
:json-schema-path="jsonSchemaPath?.length <= 0 ? `properties.${key}` : `${jsonSchemaPath}.properties.${key}`"
|
|
133
|
+
v-bind="options"
|
|
134
|
+
@update:model-value="event => onInput(event, key)"
|
|
135
|
+
/>
|
|
136
|
+
</section>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<slot name="submit">
|
|
140
|
+
<UButton :type="stage == steps.length - 1 ? 'submit' : undefined" @click="onNext">
|
|
141
|
+
{{ stage != steps.length - 1 ? 'Next' : 'Submit' }}
|
|
142
|
+
</UButton>
|
|
143
|
+
</slot>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
|
|
147
|
+
<style lang="css">
|
|
148
|
+
.stepper {
|
|
149
|
+
width: 100%;
|
|
150
|
+
height: 150px;
|
|
151
|
+
padding-bottom: 1em;
|
|
152
|
+
/* display: flex; */
|
|
153
|
+
/* align-items: center; */
|
|
154
|
+
/* justify-content: center; */
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.stepper-container {
|
|
158
|
+
padding-bottom: 1em;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.stepper-content {
|
|
162
|
+
padding-bottom: 1em;
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// import { Icon } from '@iconify/vue'
|
|
3
|
+
import {
|
|
4
|
+
StepperDescription,
|
|
5
|
+
StepperIndicator,
|
|
6
|
+
StepperItem,
|
|
7
|
+
StepperRoot,
|
|
8
|
+
StepperSeparator,
|
|
9
|
+
StepperTitle,
|
|
10
|
+
StepperTrigger
|
|
11
|
+
} from 'reka-ui'
|
|
12
|
+
|
|
13
|
+
const steps = [
|
|
14
|
+
{
|
|
15
|
+
step: 1,
|
|
16
|
+
title: 'Address',
|
|
17
|
+
description: 'Add your address here'
|
|
18
|
+
// icon: 'radix-icons:home'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
step: 2,
|
|
22
|
+
title: 'Shipping',
|
|
23
|
+
description: 'Set your preferred shipping method'
|
|
24
|
+
// icon: 'radix-icons:archive'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
step: 3,
|
|
28
|
+
title: 'Checkout',
|
|
29
|
+
description: 'Confirm your order'
|
|
30
|
+
// icon: 'radix-icons:check'
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<StepperRoot :default-value="2" class="flex gap-2 w-full max-w-[32rem]">
|
|
37
|
+
<StepperItem v-for="item in steps" :key="item.step"
|
|
38
|
+
class="w-full flex justify-center gap-2 cursor-pointer group relative px-4" :step="item.step">
|
|
39
|
+
<StepperTrigger
|
|
40
|
+
class="inline-flex border-2 shadow-sm items-center text-white bg-green9 border-green9 group-data-[state=inactive]:border-gray-200 group-data-[state=inactive]:bg-white group-data-[state=inactive]:text-stone-700 group-data-[disabled]:opacity-50 group-data-[disabled]:cursor-not-allowed justify-center rounded-full w-10 h-10 shrink-0 focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none">
|
|
41
|
+
<StepperIndicator>
|
|
42
|
+
<!-- <Icon -->
|
|
43
|
+
<!-- :icon="item.icon" -->
|
|
44
|
+
<!-- class="w-5 h-5" -->
|
|
45
|
+
<!-- /> -->
|
|
46
|
+
</StepperIndicator>
|
|
47
|
+
</StepperTrigger>
|
|
48
|
+
|
|
49
|
+
<StepperSeparator v-if="item.step !== steps[steps.length - 1].step"
|
|
50
|
+
class="absolute block top-5 left-[calc(50%+30px)] right-[calc(-50%+20px)] h-0.5 rounded-full bg-stone-300/50 shrink-0" />
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
class="absolute text-center top-full left-0 w-full mt-2 text-stone-700 dark:text-white group-data-[disabled]:opacity-50">
|
|
54
|
+
<StepperTitle class="font-medium">
|
|
55
|
+
{{ item.title }}
|
|
56
|
+
</StepperTitle>
|
|
57
|
+
<StepperDescription class="hidden sm:block text-xs">
|
|
58
|
+
{{ item.description }}
|
|
59
|
+
</StepperDescription>
|
|
60
|
+
</div>
|
|
61
|
+
</StepperItem>
|
|
62
|
+
</StepperRoot>
|
|
63
|
+
</template>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
2
|
+
export default defineNuxtConfig({
|
|
3
|
+
modules: ['../src/module', '@nuxt/ui'],
|
|
4
|
+
components: {
|
|
5
|
+
global: true,
|
|
6
|
+
dirs: ['@/components']
|
|
7
|
+
},
|
|
8
|
+
devtools: { enabled: true },
|
|
9
|
+
colorMode: {
|
|
10
|
+
preference: 'light'
|
|
11
|
+
},
|
|
12
|
+
compatibilityDate: '2025-01-31'
|
|
13
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="w-full flex justify-center">
|
|
3
|
+
<div class="w-full max-w-[800px]">
|
|
4
|
+
<ColorMode />
|
|
5
|
+
<SForm
|
|
6
|
+
v-if="schema"
|
|
7
|
+
v-model="state"
|
|
8
|
+
:schema="schema"
|
|
9
|
+
@submit="onSubmit"
|
|
10
|
+
>
|
|
11
|
+
<template #submit>
|
|
12
|
+
{{ undefined }}
|
|
13
|
+
</template>
|
|
14
|
+
</SForm>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script lang="ts" setup>
|
|
20
|
+
import ColorMode from '@/components/ColorMode.vue'
|
|
21
|
+
import formSchema from '@/assets/schema.json'
|
|
22
|
+
import { Draft2019 } from 'json-schema-library'
|
|
23
|
+
|
|
24
|
+
const parsedSchema = new Draft2019(formSchema)
|
|
25
|
+
|
|
26
|
+
const schema = ref(parsedSchema.rootSchema)
|
|
27
|
+
const state = ref(parsedSchema.getTemplate())
|
|
28
|
+
|
|
29
|
+
const onSubmit = (value: unknown) => {
|
|
30
|
+
console.log(value)
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default {}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
import { resolve } from 'pathe'
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const commit = execSync('git rev-parse --short HEAD').toString('utf-8').trim()
|
|
7
|
+
const date = Math.round(Date.now() / (1000 * 60))
|
|
8
|
+
|
|
9
|
+
const pkgPath = resolve(process.cwd(), 'package.json')
|
|
10
|
+
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
12
|
+
const pkg = JSON.parse(await fsp.readFile(pkgPath, 'utf-8').catch(() => '{}'))
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
15
|
+
pkg.version = `${pkg.version}-${date}.${commit}`
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
18
|
+
pkg.name = pkg.name + '-nightly'
|
|
19
|
+
await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
main().catch(err => {
|
|
23
|
+
console.error(err)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -xe
|
|
4
|
+
|
|
5
|
+
# Restore all git changes
|
|
6
|
+
git restore --source=HEAD --staged --worktree -- package.json pnpm-lock.yaml
|
|
7
|
+
|
|
8
|
+
# Bump versions to edge
|
|
9
|
+
pnpm jiti ./scripts/bump-nightly
|
|
10
|
+
|
|
11
|
+
# Resolve pnpm
|
|
12
|
+
pnpm i --frozen-lockfile=false
|
|
13
|
+
|
|
14
|
+
# Update token
|
|
15
|
+
if [[ ! -z ${NPM_TOKEN} ]]; then
|
|
16
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >>~/.npmrc
|
|
17
|
+
echo "registry=https://registry.npmjs.org/" >>~/.npmrc
|
|
18
|
+
echo "always-auth=true" >>~/.npmrc
|
|
19
|
+
npm whoami
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Release package
|
|
23
|
+
echo "⚡ Publishing nightly version"
|
|
24
|
+
npx npm@8.17.0 publish --access public --tolerate-republish
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
set -xe
|
|
4
|
+
|
|
5
|
+
# Restore all git changes
|
|
6
|
+
git restore --source=HEAD --staged --worktree -- package.json pnpm-lock.yaml
|
|
7
|
+
|
|
8
|
+
# Resolve pnpm
|
|
9
|
+
pnpm i --frozen-lockfile=false
|
|
10
|
+
|
|
11
|
+
# Update token
|
|
12
|
+
if [[ ! -z ${NPM_TOKEN} ]]; then
|
|
13
|
+
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >>~/.npmrc
|
|
14
|
+
echo "registry=https://registry.npmjs.org/" >>~/.npmrc
|
|
15
|
+
echo "always-auth=true" >>~/.npmrc
|
|
16
|
+
npm whoami
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Release package
|
|
20
|
+
echo "⚡ Publishing with tag latest"
|
|
21
|
+
npx npm@8.17.0 publish --tag latest --access public --tolerate-republish
|
package/src/defaults.ts
ADDED
package/src/module.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createResolver, defineNuxtModule, addComponentsDir } from '@nuxt/kit'
|
|
2
|
+
import { defaultOptions } from './defaults'
|
|
3
|
+
|
|
4
|
+
// export type * from './runtime/types'
|
|
5
|
+
|
|
6
|
+
export interface ModuleOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Prefix for components
|
|
9
|
+
* @defaultValue `S`
|
|
10
|
+
* @link https://ui3.nuxt.dev/getting-started/installation/nuxt#prefix
|
|
11
|
+
*/
|
|
12
|
+
prefix?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default defineNuxtModule<ModuleOptions>({
|
|
16
|
+
meta: {
|
|
17
|
+
name: 'SchemaForm',
|
|
18
|
+
configKey: 'schemaForm',
|
|
19
|
+
compatibility: {
|
|
20
|
+
nuxt: '>=3.13.1'
|
|
21
|
+
},
|
|
22
|
+
docs: 'https://ui3.nuxt.dev/getting-started/installation/nuxt'
|
|
23
|
+
},
|
|
24
|
+
defaults: defaultOptions,
|
|
25
|
+
setup(options, nuxt) {
|
|
26
|
+
const { resolve } = createResolver(import.meta.url)
|
|
27
|
+
nuxt.options.alias['#schema-form'] = resolve('./runtime')
|
|
28
|
+
|
|
29
|
+
addComponentsDir({
|
|
30
|
+
path: resolve('./runtime/components'),
|
|
31
|
+
prefix: options.prefix,
|
|
32
|
+
pathPrefix: false
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UnpluginOptions } from 'unplugin'
|
|
2
|
+
import type { ModuleOptions } from '../module'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This plugin injects Nuxt Schema From configuration into the runtime build so Nuxt Schema From components can
|
|
6
|
+
* access it.
|
|
7
|
+
*/
|
|
8
|
+
export default function AppConfigPlugin(_options: ModuleOptions, appConfig: Record<string, unknown>) {
|
|
9
|
+
return {
|
|
10
|
+
name: 'nuxt:schema-form:app-config',
|
|
11
|
+
enforce: 'pre',
|
|
12
|
+
resolveId(id) {
|
|
13
|
+
if (id === '#build/app.config') {
|
|
14
|
+
return 'virtual:nuxt-schema-form-app-config'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
loadInclude: id => id === 'virtual:nuxt-schema-form-app-config',
|
|
18
|
+
load() {
|
|
19
|
+
return `
|
|
20
|
+
export default ${JSON.stringify(appConfig)}
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
} satisfies UnpluginOptions
|
|
24
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { join, normalize } from 'pathe'
|
|
2
|
+
import type { UnpluginContextMeta, UnpluginOptions } from 'unplugin'
|
|
3
|
+
import { globSync } from 'tinyglobby'
|
|
4
|
+
import AutoImportComponents from 'unplugin-vue-components'
|
|
5
|
+
|
|
6
|
+
import { runtimeDir } from '../unplugin'
|
|
7
|
+
import type { NuxtUIOptions } from '../unplugin'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* This plugin adds all the Nuxt UI components as auto-imports.
|
|
11
|
+
*/
|
|
12
|
+
export default function ComponentImportPlugin(
|
|
13
|
+
framework: UnpluginContextMeta['framework'],
|
|
14
|
+
options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']> }
|
|
15
|
+
) {
|
|
16
|
+
const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') })
|
|
17
|
+
const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
|
|
18
|
+
|
|
19
|
+
const overrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'vue/components') })
|
|
20
|
+
const overrideNames = new Set(overrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
/**
|
|
24
|
+
* This plugin aims to ensure we override certain components with Vue-compatible versions:
|
|
25
|
+
* <UIcon> and <ULink> currently.
|
|
26
|
+
*/
|
|
27
|
+
{
|
|
28
|
+
name: 'nuxt:schema-form:components',
|
|
29
|
+
enforce: 'pre',
|
|
30
|
+
resolveId(id, importer) {
|
|
31
|
+
// only apply to runtime nuxt ui components
|
|
32
|
+
if (!importer || !normalize(importer).includes(runtimeDir)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// only apply to relative imports
|
|
37
|
+
if (!RELATIVE_IMPORT_RE.test(id)) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const filename = id.match(/([^/]+)\.vue$/)?.[1]
|
|
42
|
+
if (filename && overrideNames.has(`${options.prefix}${filename}`)) {
|
|
43
|
+
return join(runtimeDir, 'vue/components', `${filename}.vue`)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
AutoImportComponents[framework]({
|
|
48
|
+
dts: true,
|
|
49
|
+
exclude: [/[\\/]node_modules[\\/](?!\.pnpm|nuxt-schema-form)/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
|
|
50
|
+
resolvers: [
|
|
51
|
+
componentName => {
|
|
52
|
+
if (overrideNames.has(componentName))
|
|
53
|
+
return {
|
|
54
|
+
name: 'default',
|
|
55
|
+
from: join(runtimeDir, 'vue/components', `${componentName.slice(options.prefix.length)}.vue`)
|
|
56
|
+
}
|
|
57
|
+
if (componentNames.has(componentName))
|
|
58
|
+
return {
|
|
59
|
+
name: 'default',
|
|
60
|
+
from: join(runtimeDir, 'components', `${componentName.slice(options.prefix.length)}.vue`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
] /*satisfies*/ as UnpluginOptions[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const RELATIVE_IMPORT_RE = /^\.{1,2}\//
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { UnpluginOptions } from 'unplugin'
|
|
2
|
+
import { normalize } from 'pathe'
|
|
3
|
+
import { resolvePathSync } from 'mlly'
|
|
4
|
+
import MagicString from 'magic-string'
|
|
5
|
+
|
|
6
|
+
import { runtimeDir } from '../unplugin'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
|
|
10
|
+
*/
|
|
11
|
+
export default function NuxtEnvironmentPlugin() {
|
|
12
|
+
const stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
name: 'nuxt:schema-form',
|
|
16
|
+
enforce: 'pre',
|
|
17
|
+
resolveId(id) {
|
|
18
|
+
// this is implemented here rather than in a vite `config` hook for cross-builder support
|
|
19
|
+
if (id === '#imports') {
|
|
20
|
+
return stubPath
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
transformInclude(id) {
|
|
24
|
+
return normalize(id).includes(runtimeDir)
|
|
25
|
+
},
|
|
26
|
+
transform(code) {
|
|
27
|
+
if (code.includes('import.meta.client')) {
|
|
28
|
+
const s = new MagicString(code)
|
|
29
|
+
s.replaceAll('import.meta.client', 'true')
|
|
30
|
+
|
|
31
|
+
if (s.hasChanged()) {
|
|
32
|
+
return {
|
|
33
|
+
code: s.toString(),
|
|
34
|
+
map: s.generateMap({ hires: true })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} satisfies UnpluginOptions
|
|
40
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
|
|
4
|
+
type Properties = {
|
|
5
|
+
properties?: { [key: string]: unknown }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type SComponentProps = {
|
|
9
|
+
id: string
|
|
10
|
+
title?: string
|
|
11
|
+
renderer?: string
|
|
12
|
+
type: string
|
|
13
|
+
properties?: Properties
|
|
14
|
+
enum?: string[]
|
|
15
|
+
description?: string[]
|
|
16
|
+
jsonSchemaPath: string
|
|
17
|
+
validateFields: (fields: string[]) => Promise<boolean>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = defineProps<SComponentProps>()
|
|
21
|
+
const value = defineModel<unknown>({ required: true })
|
|
22
|
+
|
|
23
|
+
const options = computed(() => ({
|
|
24
|
+
...props,
|
|
25
|
+
jsonSchemaPath: `${props.id}${props.jsonSchemaPath}`
|
|
26
|
+
}))
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div class="s-form-group">
|
|
31
|
+
<component
|
|
32
|
+
v-bind="options"
|
|
33
|
+
:is="typeof renderer === 'string' ? renderer : renderer.name"
|
|
34
|
+
v-if="renderer"
|
|
35
|
+
v-model="value"
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<SInputField
|
|
39
|
+
v-else-if="type === 'string'"
|
|
40
|
+
v-bind="options"
|
|
41
|
+
v-model="value"
|
|
42
|
+
/>
|
|
43
|
+
<SToggle
|
|
44
|
+
v-else-if="type === 'boolean'"
|
|
45
|
+
v-bind="options"
|
|
46
|
+
v-model="value"
|
|
47
|
+
/>
|
|
48
|
+
<SObject
|
|
49
|
+
v-else-if="type === 'object'"
|
|
50
|
+
v-bind="options"
|
|
51
|
+
v-model="value"
|
|
52
|
+
/>
|
|
53
|
+
<div v-else>
|
|
54
|
+
else
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import SComponent from './SComponent.vue'
|
|
3
|
+
import convertToYup from 'json-schema-yup-transformer'
|
|
4
|
+
import type { JsonSchema } from 'json-schema-library'
|
|
5
|
+
import { computed, ref } from 'vue'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{ schema: JsonSchema }>()
|
|
8
|
+
|
|
9
|
+
const value = defineModel<Record<string, unknown>>({ required: true })
|
|
10
|
+
const formRef = ref(null)
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits(['submit'])
|
|
13
|
+
|
|
14
|
+
async function onSubmit(_event: unknown) {
|
|
15
|
+
emit('submit', value.value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const formValidationSchema = computed(() => convertToYup(props.schema))
|
|
19
|
+
|
|
20
|
+
const validateFields = async (fields: string[]) => {
|
|
21
|
+
let allValid = true
|
|
22
|
+
for (const field of fields) {
|
|
23
|
+
if (!(await formRef.value?.validate(field, { silent: true }))) {
|
|
24
|
+
allValid = false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return allValid
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div>
|
|
34
|
+
<UForm
|
|
35
|
+
ref="formRef"
|
|
36
|
+
:state="value"
|
|
37
|
+
:schema="formValidationSchema"
|
|
38
|
+
autocomplete="on"
|
|
39
|
+
@submit="onSubmit"
|
|
40
|
+
>
|
|
41
|
+
<SComponent
|
|
42
|
+
id=""
|
|
43
|
+
v-model="value"
|
|
44
|
+
v-bind="schema"
|
|
45
|
+
:validate-fields="validateFields"
|
|
46
|
+
/>
|
|
47
|
+
<slot />
|
|
48
|
+
|
|
49
|
+
<slot name="submit">
|
|
50
|
+
<UButton
|
|
51
|
+
class="btn"
|
|
52
|
+
type="submit"
|
|
53
|
+
>
|
|
54
|
+
Submit
|
|
55
|
+
</UButton>
|
|
56
|
+
</slot>
|
|
57
|
+
</UForm>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|