Skip to content

Commit

Permalink
ACustomForm
Browse files Browse the repository at this point in the history
  • Loading branch information
volar committed Nov 13, 2023
1 parent b77cade commit a93ffec
Show file tree
Hide file tree
Showing 8 changed files with 426 additions and 18 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"pinia": "2.1.7",
"postcss-html": "^1.5.0",
"postcss-prefix-selector": "^1.16.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"sass": "^1.69.5",
"shortcut-buttons-flatpickr": "^0.4.0",
"sortablejs": "^1.15.0",
Expand Down
136 changes: 136 additions & 0 deletions src/components/customForm/ACustomForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import useVuelidate from '@vuelidate/core'
import type { CustomFormElement } from '@/components/customForm/CustomForm'
import type { ValidationScope } from '@/types/Validation'
import { useCustomForm } from '@/components/customForm/useCustomForm'
import ACustomFormElement from '@/components/customForm/ACustomFormElement.vue'
import ARow from '@/components/ARow.vue'
const props = withDefaults(
defineProps<{
modelValue: { [key: string]: any }
elements: CustomFormElement[]
validationScope?: ValidationScope
pinnedCount?: number
readonly?: boolean
}>(),
{
validationScope: undefined,
pinnedCount: 1000,
readonly: false,
}
)
const emit = defineEmits<{
(e: 'update:modelValue', data: any): void
(e: 'anyChange'): void
}>()
const { t } = useI18n()
const { showAll, toggleForm } = useCustomForm()
const updateModelValue = (data: { property: string; value: any }) => {
const updated = {} as { [key: string]: any }
updated[data.property] = data.value
emit('update:modelValue', { ...props.modelValue, ...updated })
emit('anyChange')
}
const elementsPinned = computed(() => {
return props.elements.slice(0, props.pinnedCount)
})
const elementsOther = computed(() => {
return props.elements.slice(props.pinnedCount)
})
const showHideButtonText = computed(() => {
return showAll.value ? t('coreDam.asset.detail.metadataToggle.show') : t('coreDam.asset.detail.metadataToggle.hide')
})
const showHideButtonIcon = computed(() => {
return showAll.value ? 'mdi-minus' : 'mdi-plus'
})
const enableShowHide = computed(() => {
return props.elements.length > props.pinnedCount
})
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const v$ = useVuelidate({ $scope: props.validationScope })
const validate = (): Promise<boolean> => {
return v$.value.$validate()
}
defineExpose({
validate,
})
</script>

<template>
<div class="w-100">
<slot name="before-pinned" />
<VRow
v-for="element in elementsPinned"
:key="element.id"
dense
class="mt-1"
>
<VCol>
<ARow
v-if="readonly"
:title="element.name"
>
{{ modelValue[element.property] }}
</ARow>
<ACustomFormElement
v-else
:config="element"
:model-value="modelValue[element.property]"
:validation-scope="validationScope"
@update:model-value="updateModelValue"
/>
</VCol>
</VRow>
<slot name="after-pinned" />
</div>
<div
v-show="showAll"
class="w-100"
>
<VRow
v-for="element in elementsOther"
:key="element.id"
dense
class="mt-1"
>
<VCol>
<ARow
v-if="readonly"
:title="element.name"
>
{{ modelValue[element.property] }}
</ARow>
<ACustomFormElement
v-else
:config="element"
:model-value="modelValue[element.property]"
@update:model-value="updateModelValue"
/>
</VCol>
</VRow>
</div>
<VBtn
v-if="enableShowHide"
variant="text"
size="small"
class="my-2"
@click="toggleForm"
>
<VIcon :icon="showHideButtonIcon" />
{{ showHideButtonText }}
</VBtn>
</template>
166 changes: 166 additions & 0 deletions src/components/customForm/ACustomFormElement.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<script lang="ts" setup>
import { computed, isProxy, ref, toRaw } from 'vue'
import type { ErrorObject } from '@vuelidate/core'
import { useVuelidate } from '@vuelidate/core'
import type { CustomFormElement } from '@/components/customForm/CustomForm'
import type { ValidationScope } from '@/types/Validation'
import { CustomFormElementType } from '@/components/customForm/CustomFormElementTypes'
import { isEmptyObject } from '@/utils/common'
import { useValidate } from '@/validators/vuelidate/useValidate'
const props = withDefaults(
defineProps<{
modelValue: any
config: CustomFormElement
validationScope?: ValidationScope
}>(),
{
validationScope: undefined,
}
)
const emit = defineEmits<{
(e: 'update:modelValue', data: { property: string; value: any }): void
(e: 'blur', data: any): void
}>()
const fixValue = (value: any) => {
if (props.config.attributes.type === CustomFormElementType.Integer) {
return parseInt(value)
}
return value
}
const updateModelValue = (value: any) => {
emit('update:modelValue', { property: props.config.property, value: fixValue(value) })
}
const modelValueComputed = computed(() => {
const value = isProxy(props.modelValue) ? toRaw(props.modelValue) : props.modelValue
if (props.config.attributes.type === CustomFormElementType.StringArray && isEmptyObject(value)) return []
return value
})
const { maxLength, minLength, requiredIf, minValue, maxValue, stringArrayItemLength } = useValidate()
const rules = computed(() => {
const dynamicRules: Record<string, any> = {
modelValueComputed: {
required: requiredIf(props.config.attributes.required),
},
}
switch (props.config.attributes.type) {
case CustomFormElementType.String:
dynamicRules.modelValueComputed.minLength = minLength(
props.config.attributes.minValue ? props.config.attributes.minValue : 0
)
dynamicRules.modelValueComputed.maxLength = maxLength(
props.config.attributes.maxValue ? props.config.attributes.maxValue : 256
)
break
case CustomFormElementType.Integer:
dynamicRules.modelValueComputed.minValue = minValue(
props.config.attributes.minValue ? props.config.attributes.minValue : 0
)
dynamicRules.modelValueComputed.maxValue = maxValue(
props.config.attributes.maxValue ? props.config.attributes.maxValue : 9999
)
break
case CustomFormElementType.StringArray:
dynamicRules.modelValueComputed.minLength = minLength(
props.config.attributes.minCount ? props.config.attributes.minCount : 0
)
dynamicRules.modelValueComputed.maxLength = maxLength(
props.config.attributes.maxCount ? props.config.attributes.maxCount : 32
)
dynamicRules.modelValueComputed.stringArrayItemLength = stringArrayItemLength(
props.config.attributes.minValue ? props.config.attributes.minValue : 0,
props.config.attributes.maxValue ? props.config.attributes.maxValue : 256
)
break
}
return dynamicRules
})
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const v$ = useVuelidate(rules, { modelValueComputed }, { $scope: props.validationScope })
const errorMessageComputed = computed(() => {
if (v$.value.$errors.length) return [v$.value.$errors.map((item: ErrorObject) => item.$message).join(' ')]
return []
})
const counter = ref<number | undefined>(undefined)
counter.value = props.config.attributes.maxValue ?? undefined
const onBlur = () => {
emit('blur', props.modelValue)
v$.value.$touch()
}
</script>

<template>
<VTextarea
v-if="config.attributes.type === CustomFormElementType.String"
:model-value="modelValue"
auto-grow
:rows="1"
:label="config.name"
:error-messages="errorMessageComputed"
:counter="counter"
@update:model-value="updateModelValue"
@blur="onBlur"
>
<template #label>
{{ config.name
}}<span
v-if="config.attributes.required"
class="required"
/>
</template>
</VTextarea>
<VTextField
v-else-if="config.attributes.type === CustomFormElementType.Integer"
:model-value="modelValueComputed"
type="number"
:label="config.name"
:error-messages="errorMessageComputed"
@update:model-value="updateModelValue"
@blur="onBlur"
>
<template #label>
{{ config.name
}}<span
v-if="config.attributes.required"
class="required"
/>
</template>
</VTextField>
<VCombobox
v-else-if="config.attributes.type === CustomFormElementType.StringArray"
:model-value="modelValueComputed"
:label="config.name"
multiple
chips
:error-messages="errorMessageComputed"
@update:model-value="updateModelValue"
@blur="onBlur"
>
<template #label>
{{ config.name
}}<span
v-if="config.attributes.required"
class="required"
/>
</template>
</VCombobox>
<VSwitch
v-if="config.attributes.type === CustomFormElementType.Boolean && config.attributes.required === true"
:label="config.name"
:model-value="modelValueComputed"
@update:model-value="updateModelValue"
/>
<div v-if="config.attributes.type === CustomFormElementType.Boolean && config.attributes.required === false">
optional boolean todo
</div>
</template>
27 changes: 27 additions & 0 deletions src/components/customForm/CustomForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { AnzuUserAndTimeTrackingAware } from '@/types/AnzuUserAndTimeTrackingAware'
import type { ResourceNameSystemAware } from '@/types/ResourceNameSystemAware'
import type { DocId } from '@/types/common'
import type { CustomFormElementTypeType } from '@/components/customForm/CustomFormElementTypes'

export interface CustomFormElement extends AnzuUserAndTimeTrackingAware, ResourceNameSystemAware {
id: DocId
property: string
name: string
position: number
attributes: CustomFormElementAttributes
}

export interface CustomFormElementAttributes {
type: CustomFormElementTypeType
minValue: number | null
maxValue: number | null
minCount: number | null
maxCount: number | null
required: boolean
searchable: boolean
readonly: boolean
}

export interface CustomFormDataAware {
customData: { [key: string]: any }
}
43 changes: 43 additions & 0 deletions src/components/customForm/CustomFormElementTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { ValueObjectOption } from '@/types/ValueObject'

export const CustomFormElementType = {
String: 'string',
Integer: 'integer',
StringArray: 'string_array',
Boolean: 'boolean',
} as const
export const CustomFormElementTypeDefault = CustomFormElementType.String
export type CustomFormElementTypeType = (typeof CustomFormElementType)[keyof typeof CustomFormElementType]

export function useCustomFormElementType() {
const { t } = useI18n()
const customFormElementTypeOptions = ref<ValueObjectOption<CustomFormElementTypeType>[]>([
{
value: CustomFormElementType.String,
title: t('common.customFormElement.type.string'),
},
{
value: CustomFormElementType.Integer,
title: t('common.customFormElement.type.integer'),
},
{
value: CustomFormElementType.StringArray,
title: t('common.customFormElement.type.stringArray'),
},
{
value: CustomFormElementType.Boolean,
title: t('common.customFormElement.type.boolean'),
},
])

const getCustomFormElementTypeOption = (value: CustomFormElementTypeType) => {
return customFormElementTypeOptions.value.find((item) => item.value === value)
}

return {
customFormElementTypeOptions,
getCustomFormElementTypeOption,
}
}
14 changes: 14 additions & 0 deletions src/components/customForm/useCustomForm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ref } from 'vue'

const showAll = ref(false)

export function useCustomForm() {
const toggleForm = () => {
showAll.value = !showAll.value
}

return {
showAll,
toggleForm,
}
}
Loading

0 comments on commit a93ffec

Please sign in to comment.