UNPKG

@jsonforms/vue-vuetify

Version:

Vue Vuetify renderers for JSON Forms

484 lines (426 loc) 11.2 kB
import { aliases as faIcons } from '@/icons/fa'; import type { IconAliases } from '@/icons/icons'; import { aliases as mdiIcons } from '@/icons/mdi'; import { Resolve, arrayDefaultTranslations, combinatorDefaultTranslations, composePaths, computeLabel, defaultJsonFormsI18nState, getArrayTranslations, getCombinatorTranslations, getFirstPrimitiveProp, isDescriptionHidden, type ControlElement, type DispatchPropsOfControl, type DispatchPropsOfMultiEnumControl, type JsonFormsSubStates, type JsonSchema, type UISchemaElement, } from '@jsonforms/core'; import type Ajv from 'ajv'; import type { ErrorObject } from 'ajv'; import cloneDeep from 'lodash/cloneDeep'; import debounce from 'lodash/debounce'; import get from 'lodash/get'; import isPlainObject from 'lodash/isPlainObject'; import merge from 'lodash/merge'; import { computed, inject, provide, ref, type ComputedRef, type InjectionKey, } from 'vue'; import type { IconOptions } from 'vuetify'; import { useStyles } from '../styles'; export const IconSymbol: InjectionKey<Required<IconOptions>> = Symbol.for('vuetify:icons'); export const useControlAppliedOptions = < T extends { config: any; uischema: UISchemaElement }, I extends { control: ComputedRef<T>; }, >( input: I, ) => { return computed(() => merge( {}, cloneDeep(input.control.value.config), cloneDeep(input.control.value.uischema.options), ), ); }; export const useLayoutAppliedOptions = < T extends { config: any; uischema: UISchemaElement }, I extends { layout: ComputedRef<T>; }, >( input: I, ) => { return computed(() => merge( {}, cloneDeep(input.layout.value.config), cloneDeep(input.layout.value.uischema.options), ), ); }; export const useComputedLabel = < T extends { label: string; required: boolean }, I extends { control: ComputedRef<T> }, >( input: I, appliedOptions: ReturnType<typeof useControlAppliedOptions>, ) => { return computed((): string => { return computeLabel( input.control.value.label, input.control.value.required, !!appliedOptions.value?.hideRequiredAsterisk, ); }); }; /** * Adds styles, appliedOptions and vuetifyProps */ export const useVuetifyLabel = < T extends { uischema: UISchemaElement; config: any; }, I extends { label: ComputedRef<T>; }, >( input: I, ) => { const styles = useStyles(input.label.value.uischema); const appliedOptions = computed(() => merge( {}, cloneDeep(input.label.value.config), cloneDeep(input.label.value.uischema.options), ), ); const vuetifyProps = (path: string) => { const props = get(appliedOptions.value?.vuetify, path); return props && isPlainObject(props) ? props : {}; }; return { ...input, appliedOptions, vuetifyProps, styles, }; }; /** * Adds styles, isFocused, appliedOptions and onChange */ export const useVuetifyControl = < T extends { uischema: ControlElement; path: string; config: any; label: string; description: string; required: boolean; errors: string; id: string; visible: boolean; }, I extends { control: ComputedRef<T>; } & (DispatchPropsOfControl | DispatchPropsOfMultiEnumControl), >( input: I, adaptValue: (target: any) => any = (v) => v, debounceWait?: number, ) => { const touched = ref(false); const changeEmitter = typeof debounceWait === 'number' && (input as DispatchPropsOfControl).handleChange ? debounce((input as DispatchPropsOfControl).handleChange, debounceWait) : (input as DispatchPropsOfControl).handleChange; const onChange = (value: any) => { if (changeEmitter) { changeEmitter(input.control.value.path, adaptValue(value)); } }; const appliedOptions = useControlAppliedOptions(input); const isFocused = ref(false); const handleFocus = () => { isFocused.value = true; }; const handleBlur = () => { touched.value = true; isFocused.value = false; }; const filteredErrors = computed(() => { return touched.value || !appliedOptions.value.enableFilterErrorsBeforeTouch ? input.control.value.errors : ''; }); const persistentHint = (): boolean => { return !isDescriptionHidden( input.control.value.visible, input.control.value.description, isFocused.value, !!appliedOptions.value?.showUnfocusedDescription, ); }; const computedLabel = useComputedLabel(input, appliedOptions); const controlWrapper = computed(() => { const { id, description, errors, label, visible, required } = input.control.value; return { id, description, errors, label, visible, required }; }); const styles = useStyles(input.control.value.uischema); const vuetifyProps = (path: string) => { const props = get(appliedOptions.value?.vuetify, path); return props && isPlainObject(props) ? props : {}; }; const overwrittenControl = computed(() => { return { ...input.control.value, errors: filteredErrors.value, }; }); const rawErrors = computed(() => input.control.value.errors); return { ...input, control: overwrittenControl, styles, isFocused, appliedOptions, controlWrapper, onChange, vuetifyProps, persistentHint, computedLabel, touched, handleBlur, handleFocus, rawErrors, }; }; export const useCombinatorTranslations = < T extends { i18nKeyPrefix: string; label: string; }, I extends { control: ComputedRef<T>; }, >( input: I, ) => { const jsonforms = inject<JsonFormsSubStates>('jsonforms'); const translations = getCombinatorTranslations( jsonforms?.i18n?.translate ?? defaultJsonFormsI18nState.translate, combinatorDefaultTranslations, input.control.value.i18nKeyPrefix, input.control.value.label, ); const overwrittenControl = computed(() => { return { ...input.control.value, translations, }; }); return { ...input, control: overwrittenControl, }; }; export const useJsonForms = () => { const jsonforms = inject<JsonFormsSubStates>('jsonforms'); if (!jsonforms) { throw new Error( "'jsonforms couldn't be injected. Are you within JSON Forms?", ); } return jsonforms; }; export const useTranslator = () => { const jsonforms = useJsonForms(); if (!jsonforms.i18n || !jsonforms.i18n.translate) { throw new Error( "'jsonforms i18n couldn't be injected. Are you within JSON Forms?", ); } const translate = computed(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return jsonforms.i18n!.translate!; }); return translate; }; /** * Adds styles and appliedOptions */ export const useVuetifyLayout = < T extends { config: any; uischema: UISchemaElement }, I extends { layout: ComputedRef<T> }, >( input: I, ) => { const appliedOptions = useLayoutAppliedOptions(input); const vuetifyProps = (path: string) => { const props = get(appliedOptions.value?.vuetify, path); return props && isPlainObject(props) ? props : {}; }; return { ...input, styles: useStyles(input.layout.value.uischema), appliedOptions, vuetifyProps, }; }; /** * Adds styles, appliedOptions and childUiSchema */ export const useVuetifyArrayControl = < T extends { label: string; required: boolean; config: any; uischema: UISchemaElement; schema: JsonSchema; data: any; childErrors: ErrorObject[]; i18nKeyPrefix: string; }, I extends { control: ComputedRef<T>; }, >( input: I, ) => { const appliedOptions = useControlAppliedOptions(input); const computedLabel = useComputedLabel(input, appliedOptions); const vuetifyProps = (path: string) => { const props = get(appliedOptions.value?.vuetify, path); return props && isPlainObject(props) ? props : {}; }; const childLabelForIndex = (index: number | null) => { if (index === null) { return ''; } const childLabelProp = input.control.value.uischema.options?.childLabelProp ?? getFirstPrimitiveProp(input.control.value.schema); if (!childLabelProp) { return `${index}`; } const labelValue = Resolve.data( input.control.value.data, composePaths(`${index}`, childLabelProp), ); if ( labelValue === undefined || labelValue === null || Number.isNaN(labelValue) ) { return ''; } return `${labelValue}`; }; const filteredChildErrors = computed(() => { // supress childErrors unless touch filtering is disabled // otherwise all child errors will show, irrespective of their control touch state const filtered: ErrorObject[] = appliedOptions.value ?.enableFilterErrorsBeforeTouch ? [] : input.control.value.childErrors; return filtered; }); const jsonforms = inject<JsonFormsSubStates>('jsonforms'); const translations = getArrayTranslations( jsonforms?.i18n?.translate ?? defaultJsonFormsI18nState.translate, arrayDefaultTranslations, input.control.value.i18nKeyPrefix, input.control.value.label, ); const overwrittenControl = computed(() => { return { ...input.control.value, childErrors: filteredChildErrors.value, translations, }; }); return { ...input, control: overwrittenControl, styles: useStyles(input.control.value.uischema), appliedOptions, childLabelForIndex, computedLabel, vuetifyProps, rawChildErrors: input.control.value.childErrors, }; }; /** * Adds styles and appliedOptions */ export const useVuetifyBasicControl = < T extends { config: any; uischema: UISchemaElement }, I extends { control: ComputedRef<T>; }, >( input: I, ) => { const appliedOptions = useControlAppliedOptions(input); const vuetifyProps = (path: string) => { const props = get(appliedOptions.value?.vuetify, path); return props && isPlainObject(props) ? props : {}; }; return { ...input, styles: useStyles(input.control.value.uischema), appliedOptions, vuetifyProps, }; }; /** * Extracts Ajv from JSON Forms */ export const useAjv = () => { const jsonforms = useJsonForms(); // should always exist return jsonforms.core?.ajv as Ajv; }; export interface NestedInfo { level: number; parentElement?: 'array' | 'object'; } export const useNested = (element: false | 'array' | 'object'): NestedInfo => { const nestedInfo = inject<NestedInfo>('jsonforms.nestedInfo', { level: 0 }); if (element) { provide('jsonforms.nestedInfo', { level: nestedInfo.level + 1, parentElement: element, }); } return nestedInfo; }; export const useIcons = () => { const iconSet = computed<IconAliases>(() => { const icons = inject(IconSymbol); if (!icons) throw new Error('Missing Vuetify Icons provide!'); let result = mdiIcons; // default const overrides = icons.aliases; if (icons.defaultSet === 'fa') { result = faIcons; } return overrides ? { ...result, ...overrides } : result; }); return { current: iconSet, }; };