@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.
Files changed (40) hide show
  1. package/.github/semantic.yml +15 -0
  2. package/.github/workflows/ci.yml +79 -0
  3. package/.github/workflows/release.yml +62 -0
  4. package/LICENSE.md +9 -0
  5. package/README.md +11 -0
  6. package/eslint.config.mjs +73 -0
  7. package/package.json +66 -0
  8. package/playground/app.vue +5 -0
  9. package/playground/assets/schema-custom.json +111 -0
  10. package/playground/assets/schema-easy.json +16 -0
  11. package/playground/assets/schema.json +116 -0
  12. package/playground/components/ColorMode.vue +26 -0
  13. package/playground/components/CustomInput.vue +7 -0
  14. package/playground/components/MultiStage.vue +164 -0
  15. package/playground/components/Stepper.vue +63 -0
  16. package/playground/nuxt.config.ts +13 -0
  17. package/playground/package.json +8 -0
  18. package/playground/pages/index.vue +32 -0
  19. package/playground/tailwind.config.js +1 -0
  20. package/playground/tsconfig.json +4 -0
  21. package/pnpm-workspace.yaml +2 -0
  22. package/prettier.config.mjs +8 -0
  23. package/scripts/bump-nightly.ts +25 -0
  24. package/scripts/release-nightly.sh +24 -0
  25. package/scripts/release.sh +21 -0
  26. package/src/defaults.ts +3 -0
  27. package/src/module.ts +35 -0
  28. package/src/plugins/app-config.ts +24 -0
  29. package/src/plugins/components.ts +68 -0
  30. package/src/plugins/nuxt-environment.ts +40 -0
  31. package/src/runtime/components/SComponent.vue +57 -0
  32. package/src/runtime/components/SForm.vue +59 -0
  33. package/src/runtime/components/SInputField.vue +25 -0
  34. package/src/runtime/components/SObject.vue +34 -0
  35. package/src/runtime/components/SSelect.vue +26 -0
  36. package/src/runtime/components/STextarea.vue +24 -0
  37. package/src/runtime/components/SToggle.vue +21 -0
  38. package/src/runtime/types/utils.ts +44 -0
  39. package/src/unplugin.ts +46 -0
  40. 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,8 @@
1
+ {
2
+ "dependencies": {
3
+ "@nuxt/ui": "^2.21.0",
4
+ "nuxt": "^3.15.4",
5
+ "@ederzeel/nuxt-schema-form": "../",
6
+ "reka-ui": "1.0.0-alpha.9"
7
+ }
8
+ }
@@ -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,4 @@
1
+ {
2
+ // https://nuxt.com/docs/guide/concepts/typescript
3
+ "extends": "./.nuxt/tsconfig.json"
4
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - 'playground'
@@ -0,0 +1,8 @@
1
+ export default {
2
+ semi: false,
3
+ singleQuote: true,
4
+ printWidth: 120,
5
+ trailingComma: 'none',
6
+ endOfLine: 'auto',
7
+ arrowParens: 'avoid'
8
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ export const defaultOptions = {
2
+ prefix: 'S'
3
+ }
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>