fix: warning in console

This commit is contained in:
zernonia 2024-04-22 23:14:57 +08:00
parent d674549b67
commit f255983693
6 changed files with 269 additions and 147 deletions

View File

@ -2,7 +2,7 @@
import * as z from 'zod' import * as z from 'zod'
import { computed, provide } from 'vue' import { computed, provide } from 'vue'
import { PlusIcon, TrashIcon } from 'lucide-vue-next' import { PlusIcon, TrashIcon } from 'lucide-vue-next'
import { FieldArray, FieldContextKey, useField, useFieldArray } from 'vee-validate' import { FieldArray, FieldContextKey, useField } from 'vee-validate'
import type { Config, ConfigItem } from './interface' import type { Config, ConfigItem } from './interface'
import { beautifyObjectName, getBaseType } from './utils' import { beautifyObjectName, getBaseType } from './utils'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
@ -10,7 +10,7 @@ import AutoFormLabel from './AutoFormLabel.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/default/ui/accordion' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/default/ui/accordion'
import { Button } from '@/lib/registry/default/ui/button' import { Button } from '@/lib/registry/default/ui/button'
import { Separator } from '@/lib/registry/default/ui/separator' import { Separator } from '@/lib/registry/default/ui/separator'
import { FormMessage } from '@/lib/registry/default/ui/form' import { FormItem, FormMessage } from '@/lib/registry/default/ui/form'
const props = defineProps<{ const props = defineProps<{
fieldName: string fieldName: string
@ -58,51 +58,53 @@ provide(FieldContextKey, fieldContext)
<template> <template>
<FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName"> <FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName">
<slot v-bind="props"> <slot v-bind="props">
<Accordion type="multiple" class="w-full" collapsible :disabled="disabled"> <Accordion type="multiple" class="w-full" collapsible :disabled="disabled" as-child>
<AccordionItem :value="fieldName" class="border-none"> <FormItem>
<AccordionTrigger> <AccordionItem :value="fieldName" class="border-none">
<AutoFormLabel class="text-base" :required="required"> <AccordionTrigger>
{{ schema?.description || beautifyObjectName(fieldName) }} <AutoFormLabel class="text-base" :required="required">
</AutoFormLabel> {{ schema?.description || beautifyObjectName(fieldName) }}
</AccordionTrigger> </AutoFormLabel>
</AccordionTrigger>
<AccordionContent> <AccordionContent>
<template v-for="(field, index) of fields" :key="field.key"> <template v-for="(field, index) of fields" :key="field.key">
<div class="mb-4 p-1"> <div class="mb-4 p-1">
<AutoFormField <AutoFormField
:field-name="`${fieldName}[${index}]`" :field-name="`${fieldName}[${index}]`"
:label="fieldName" :label="fieldName"
:shape="itemShape!" :shape="itemShape!"
:config="config as ConfigItem" :config="config as ConfigItem"
/> />
<div class="!my-4 flex justify-end"> <div class="!my-4 flex justify-end">
<Button <Button
type="button" type="button"
size="icon" size="icon"
variant="secondary" variant="secondary"
@click="remove(index)" @click="remove(index)"
> >
<TrashIcon :size="16" /> <TrashIcon :size="16" />
</Button> </Button>
</div>
<Separator v-if="!field.isLast" />
</div> </div>
<Separator v-if="!field.isLast" /> </template>
</div>
</template>
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
class="mt-4 flex items-center" class="mt-4 flex items-center"
@click="push(null)" @click="push(null)"
> >
<PlusIcon class="mr-2" :size="16" /> <PlusIcon class="mr-2" :size="16" />
Add Add
</Button> </Button>
</AccordionContent> </AccordionContent>
<FormMessage /> <FormMessage />
</AccordionItem> </AccordionItem>
</FormItem>
</Accordion> </Accordion>
</slot> </slot>
</FieldArray> </FieldArray>

View File

@ -1,63 +1,46 @@
import type * as z from 'zod' import type * as z from 'zod'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, inject, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { FieldContextKey, FormContextKey } from 'vee-validate' import { useFieldValue, useFormValues } from 'vee-validate'
import { createContext } from 'radix-vue' import { createContext } from 'radix-vue'
import { type Dependency, DependencyType, type EnumValues } from './interface' import { type Dependency, DependencyType, type EnumValues } from './interface'
import { getIndexIfArray } from './utils' import { getFromPath, getIndexIfArray } from './utils'
export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies') export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies')
function getValueByPath<T extends Record<string, any>>(obj: T, path: string): any {
const keys = path.split('.')
let value = obj
for (const key of keys) {
if (value && typeof value === 'object' && key in value)
value = value[key]
else
return undefined
}
return value
}
export default function useDependencies( export default function useDependencies(
fieldName: string, fieldName: string,
) { ) {
const form = inject(FormContextKey) const form = useFormValues()
const field = inject(FieldContextKey) // parsed test[0].age => test.age
const currentFieldName = fieldName.replace(/\[\d+\]/g, '') const currentFieldName = fieldName.replace(/\[\d+\]/g, '')
const currentFieldValue = useFieldValue<any>(fieldName)
if (!form) if (!form)
throw new Error('useDependencies should be used within <AutoForm>') throw new Error('useDependencies should be used within <AutoForm>')
const { controlledValues } = form
const dependencies = injectDependencies() const dependencies = injectDependencies()
const isDisabled = ref(false) const isDisabled = ref(false)
const isHidden = ref(false) const isHidden = ref(false)
const isRequired = ref(false) const isRequired = ref(false)
const overrideOptions = ref<EnumValues | undefined>() const overrideOptions = ref<EnumValues | undefined>()
const currentFieldValue = computed(() => field?.value.value)
const currentFieldDependencies = computed(() => dependencies.value?.filter( const currentFieldDependencies = computed(() => dependencies.value?.filter(
dependency => dependency.targetField === currentFieldName, dependency => dependency.targetField === currentFieldName,
)) ))
function getSourceValue(dep: Dependency<any>) { function getSourceValue(dep: Dependency<any>) {
const source = dep.sourceField as string const source = dep.sourceField as string
const lastKey = source.split('.').at(-1) const index = getIndexIfArray(fieldName) ?? -1
if (source.includes('.') && lastKey) { const [sourceLast, ...sourceInitial] = source.split('.').toReversed()
if (Array.isArray(field?.value.value)) { const [_targetLast, ...targetInitial] = (dep.targetField as string).split('.').toReversed()
const index = getIndexIfArray(fieldName) ?? -1
return field?.value.value[index][lastKey]
}
return getValueByPath(form!.values, source) if (index >= 0 && sourceInitial.join(',') === targetInitial.join(',')) {
const [_currentLast, ...currentInitial] = fieldName.split('.').toReversed()
return getFromPath(form.value, currentInitial.join('.') + sourceLast)
} }
return controlledValues.value[source as string] return getFromPath(form.value, source)
} }
const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep))) const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep)))
@ -73,7 +56,6 @@ export default function useDependencies(
resetConditionState() resetConditionState()
currentFieldDependencies.value?.forEach((dep) => { currentFieldDependencies.value?.forEach((dep) => {
const sourceValue = getSourceValue(dep) const sourceValue = getSourceValue(dep)
const conditionMet = dep.when(sourceValue, currentFieldValue.value) const conditionMet = dep.when(sourceValue, currentFieldValue.value)
switch (dep.type) { switch (dep.type) {

View File

@ -92,3 +92,80 @@ export function getObjectFormSchema(
} }
return schema as z.ZodObject<any, any> return schema as z.ZodObject<any, any>
} }
function isIndex(value: unknown): value is number {
return Number(value) >= 0
}
/**
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
*/
export function normalizeFormPath(path: string): string {
const pathArr = path.split('.')
if (!pathArr.length)
return ''
let fullPath = String(pathArr[0])
for (let i = 1; i < pathArr.length; i++) {
if (isIndex(pathArr[i])) {
fullPath += `[${pathArr[i]}]`
continue
}
fullPath += `.${pathArr[i]}`
}
return fullPath
}
type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord }
/**
* Checks if the path opted out of nested fields using `[fieldName]` syntax
*/
export function isNotNestedPath(path: string) {
return /^\[.+\]$/i.test(path)
}
function isObject(obj: unknown): obj is Record<string, unknown> {
return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
}
function isContainerValue(value: unknown): value is Record<string, unknown> {
return isObject(value) || Array.isArray(value)
}
function cleanupNonNestedPath(path: string) {
if (isNotNestedPath(path))
return path.replace(/\[|\]/gi, '')
return path
}
/**
* Gets a nested property value from an object
*/
export function getFromPath<TValue = unknown>(object: NestedRecord | undefined, path: string): TValue | undefined
export function getFromPath<TValue = unknown, TFallback = TValue>(
object: NestedRecord | undefined,
path: string,
fallback?: TFallback,
): TValue | TFallback
export function getFromPath<TValue = unknown, TFallback = TValue>(
object: NestedRecord | undefined,
path: string,
fallback?: TFallback,
): TValue | TFallback | undefined {
if (!object)
return fallback
if (isNotNestedPath(path))
return object[cleanupNonNestedPath(path)] as TValue | undefined
const resolvedValue = (path || '')
.split(/\.|\[(\d+)\]/)
.filter(Boolean)
.reduce((acc, propKey) => {
if (isContainerValue(acc) && propKey in acc)
return acc[propKey]
return fallback
}, object as unknown)
return resolvedValue as TValue | undefined
}

View File

@ -1,8 +1,8 @@
<script setup lang="ts" generic="T extends z.ZodAny"> <script setup lang="ts" generic="T extends z.ZodAny">
import * as z from 'zod' import * as z from 'zod'
import { computed, provide } from 'vue' import { computed, provide } from 'vue'
import { PlusIcon, TrashIcon } from '@radix-icons/vue' import { PlusIcon, TrashIcon } from 'lucide-vue-next'
import { FieldArray, FieldContextKey, useField, useFieldArray } from 'vee-validate' import { FieldArray, FieldContextKey, useField } from 'vee-validate'
import type { Config, ConfigItem } from './interface' import type { Config, ConfigItem } from './interface'
import { beautifyObjectName, getBaseType } from './utils' import { beautifyObjectName, getBaseType } from './utils'
import AutoFormField from './AutoFormField.vue' import AutoFormField from './AutoFormField.vue'
@ -10,7 +10,7 @@ import AutoFormLabel from './AutoFormLabel.vue'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion' import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/lib/registry/new-york/ui/accordion'
import { Button } from '@/lib/registry/new-york/ui/button' import { Button } from '@/lib/registry/new-york/ui/button'
import { Separator } from '@/lib/registry/new-york/ui/separator' import { Separator } from '@/lib/registry/new-york/ui/separator'
import { FormMessage } from '@/lib/registry/new-york/ui/form' import { FormItem, FormMessage } from '@/lib/registry/new-york/ui/form'
const props = defineProps<{ const props = defineProps<{
fieldName: string fieldName: string
@ -58,51 +58,53 @@ provide(FieldContextKey, fieldContext)
<template> <template>
<FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName"> <FieldArray v-slot="{ fields, remove, push }" as="section" :name="fieldName">
<slot v-bind="props"> <slot v-bind="props">
<Accordion type="multiple" class="w-full" collapsible :disabled="disabled"> <Accordion type="multiple" class="w-full" collapsible :disabled="disabled" as-child>
<AccordionItem :value="fieldName" class="border-none"> <FormItem>
<AccordionTrigger> <AccordionItem :value="fieldName" class="border-none">
<AutoFormLabel class="text-base" :required="required"> <AccordionTrigger>
{{ schema?.description || beautifyObjectName(fieldName) }} <AutoFormLabel class="text-base" :required="required">
</AutoFormLabel> {{ schema?.description || beautifyObjectName(fieldName) }}
</AccordionTrigger> </AutoFormLabel>
</AccordionTrigger>
<AccordionContent> <AccordionContent>
<template v-for="(field, index) of fields" :key="field.key"> <template v-for="(field, index) of fields" :key="field.key">
<div class="mb-4 p-[1px]"> <div class="mb-4 p-1">
<AutoFormField <AutoFormField
:field-name="`${fieldName}[${index}]`" :field-name="`${fieldName}[${index}]`"
:label="fieldName" :label="fieldName"
:shape="itemShape!" :shape="itemShape!"
:config="config as ConfigItem" :config="config as ConfigItem"
/> />
<div class="!my-4 flex justify-end"> <div class="!my-4 flex justify-end">
<Button <Button
type="button" type="button"
size="icon" size="icon"
variant="secondary" variant="secondary"
@click="remove(index)" @click="remove(index)"
> >
<TrashIcon /> <TrashIcon :size="16" />
</Button> </Button>
</div>
<Separator v-if="!field.isLast" />
</div> </div>
<Separator v-if="!field.isLast" /> </template>
</div>
</template>
<Button <Button
type="button" type="button"
variant="secondary" variant="secondary"
class="mt-4 flex items-center" class="mt-4 flex items-center"
@click="push(null)" @click="push(null)"
> >
<PlusIcon class="mr-2" /> <PlusIcon class="mr-2" :size="16" />
Add Add
</Button> </Button>
</AccordionContent> </AccordionContent>
<FormMessage /> <FormMessage />
</AccordionItem> </AccordionItem>
</FormItem>
</Accordion> </Accordion>
</slot> </slot>
</FieldArray> </FieldArray>

View File

@ -1,63 +1,46 @@
import type * as z from 'zod' import type * as z from 'zod'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, inject, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { FieldContextKey, FormContextKey } from 'vee-validate' import { useFieldValue, useFormValues } from 'vee-validate'
import { createContext } from 'radix-vue' import { createContext } from 'radix-vue'
import { type Dependency, DependencyType, type EnumValues } from './interface' import { type Dependency, DependencyType, type EnumValues } from './interface'
import { getIndexIfArray } from './utils' import { getFromPath, getIndexIfArray } from './utils'
export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies') export const [injectDependencies, provideDependencies] = createContext<Ref<Dependency<z.infer<z.ZodObject<any>>>[] | undefined>>('AutoFormDependencies')
function getValueByPath<T extends Record<string, any>>(obj: T, path: string): any {
const keys = path.split('.')
let value = obj
for (const key of keys) {
if (value && typeof value === 'object' && key in value)
value = value[key]
else
return undefined
}
return value
}
export default function useDependencies( export default function useDependencies(
fieldName: string, fieldName: string,
) { ) {
const form = inject(FormContextKey) const form = useFormValues()
const field = inject(FieldContextKey) // parsed test[0].age => test.age
const currentFieldName = fieldName.replace(/\[\d+\]/g, '') const currentFieldName = fieldName.replace(/\[\d+\]/g, '')
const currentFieldValue = useFieldValue<any>(fieldName)
if (!form) if (!form)
throw new Error('useDependencies should be used within <AutoForm>') throw new Error('useDependencies should be used within <AutoForm>')
const { controlledValues } = form
const dependencies = injectDependencies() const dependencies = injectDependencies()
const isDisabled = ref(false) const isDisabled = ref(false)
const isHidden = ref(false) const isHidden = ref(false)
const isRequired = ref(false) const isRequired = ref(false)
const overrideOptions = ref<EnumValues | undefined>() const overrideOptions = ref<EnumValues | undefined>()
const currentFieldValue = computed(() => field?.value.value)
const currentFieldDependencies = computed(() => dependencies.value?.filter( const currentFieldDependencies = computed(() => dependencies.value?.filter(
dependency => dependency.targetField === currentFieldName, dependency => dependency.targetField === currentFieldName,
)) ))
function getSourceValue(dep: Dependency<any>) { function getSourceValue(dep: Dependency<any>) {
const source = dep.sourceField as string const source = dep.sourceField as string
const lastKey = source.split('.').at(-1) const index = getIndexIfArray(fieldName) ?? -1
if (source.includes('.') && lastKey) { const [sourceLast, ...sourceInitial] = source.split('.').toReversed()
if (Array.isArray(field?.value.value)) { const [_targetLast, ...targetInitial] = (dep.targetField as string).split('.').toReversed()
const index = getIndexIfArray(fieldName) ?? -1
return field?.value.value[index][lastKey]
}
return getValueByPath(form!.values, source) if (index >= 0 && sourceInitial.join(',') === targetInitial.join(',')) {
const [_currentLast, ...currentInitial] = fieldName.split('.').toReversed()
return getFromPath(form.value, currentInitial.join('.') + sourceLast)
} }
return controlledValues.value[source as string] return getFromPath(form.value, source)
} }
const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep))) const sourceFieldValues = computed(() => currentFieldDependencies.value?.map(dep => getSourceValue(dep)))
@ -73,7 +56,6 @@ export default function useDependencies(
resetConditionState() resetConditionState()
currentFieldDependencies.value?.forEach((dep) => { currentFieldDependencies.value?.forEach((dep) => {
const sourceValue = getSourceValue(dep) const sourceValue = getSourceValue(dep)
const conditionMet = dep.when(sourceValue, currentFieldValue.value) const conditionMet = dep.when(sourceValue, currentFieldValue.value)
switch (dep.type) { switch (dep.type) {

View File

@ -92,3 +92,80 @@ export function getObjectFormSchema(
} }
return schema as z.ZodObject<any, any> return schema as z.ZodObject<any, any>
} }
function isIndex(value: unknown): value is number {
return Number(value) >= 0
}
/**
* Constructs a path with dot paths for arrays to use brackets to be compatible with vee-validate path syntax
*/
export function normalizeFormPath(path: string): string {
const pathArr = path.split('.')
if (!pathArr.length)
return ''
let fullPath = String(pathArr[0])
for (let i = 1; i < pathArr.length; i++) {
if (isIndex(pathArr[i])) {
fullPath += `[${pathArr[i]}]`
continue
}
fullPath += `.${pathArr[i]}`
}
return fullPath
}
type NestedRecord = Record<string, unknown> | { [k: string]: NestedRecord }
/**
* Checks if the path opted out of nested fields using `[fieldName]` syntax
*/
export function isNotNestedPath(path: string) {
return /^\[.+\]$/i.test(path)
}
function isObject(obj: unknown): obj is Record<string, unknown> {
return obj !== null && !!obj && typeof obj === 'object' && !Array.isArray(obj)
}
function isContainerValue(value: unknown): value is Record<string, unknown> {
return isObject(value) || Array.isArray(value)
}
function cleanupNonNestedPath(path: string) {
if (isNotNestedPath(path))
return path.replace(/\[|\]/gi, '')
return path
}
/**
* Gets a nested property value from an object
*/
export function getFromPath<TValue = unknown>(object: NestedRecord | undefined, path: string): TValue | undefined
export function getFromPath<TValue = unknown, TFallback = TValue>(
object: NestedRecord | undefined,
path: string,
fallback?: TFallback,
): TValue | TFallback
export function getFromPath<TValue = unknown, TFallback = TValue>(
object: NestedRecord | undefined,
path: string,
fallback?: TFallback,
): TValue | TFallback | undefined {
if (!object)
return fallback
if (isNotNestedPath(path))
return object[cleanupNonNestedPath(path)] as TValue | undefined
const resolvedValue = (path || '')
.split(/\.|\[(\d+)\]/)
.filter(Boolean)
.reduce((acc, propKey) => {
if (isContainerValue(acc) && propKey in acc)
return acc[propKey]
return fallback
}, object as unknown)
return resolvedValue as TValue | undefined
}