Skip to content

Commit

Permalink
feat: 初始化动态表单
Browse files Browse the repository at this point in the history
  • Loading branch information
Yan Heng committed Aug 11, 2023
1 parent e7a84c6 commit 9f8bfb4
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 40 deletions.
7 changes: 0 additions & 7 deletions main/src/components/Form.vue

This file was deleted.

5 changes: 5 additions & 0 deletions main/src/constants/inject-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { InjectionKey } from 'vue';

import { FormState } from '@/form/types';

export const FormStateKey: InjectionKey<FormState> = Symbol('FORM_STATE');
105 changes: 105 additions & 0 deletions main/src/form/Container.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script setup lang="ts">
import { computed, toRefs } from 'vue';
import { injectStrict } from '@pictode/vue-aide';
import { ElFormItem } from 'element-plus';
import { FormStateKey } from '../constants/inject-key';
import { ChildConfig, FormSize, FormValue, OnChangeHandler } from './types';
const props = withDefaults(
defineProps<{
config: ChildConfig;
formModel: FormValue;
prop?: string;
labelWidth?: string;
size?: FormSize;
}>(),
{
prop: '',
size: 'small',
}
);
const formState = injectStrict(FormStateKey);
const { config, prop, formModel } = toRefs(props);
const name = computed<string | number>(() => config.value.name || '');
const itemProp = computed<string>(() => {
let result: string | number = '';
if (name.value) {
result = name.value;
} else {
return prop.value;
}
return `${prop.value}${prop.value ? '.' : ''}${result}`;
});
const type = computed<string>(() => {
let { type } = config.value;
if (typeof type === 'function') {
type = type(formState, { model: formModel });
}
if (type === 'form') {
return '';
}
return type?.replace(/([A-Z])/g, '-$1').toLowerCase();
});
const handleChange = (onChange?: OnChangeHandler, value?: FormValue | number | string): any => {
if (typeof onChange !== 'function') {
return;
}
return onChange(formState, value, {
initValue: formState.initValues,
model: formModel,
parent: formState.parentValues,
formValue: formModel.value,
prop: itemProp.value,
config: config.value,
});
};
const onChangeHandler = async (v: FormValue) => {
const { onChange, name } = config.value;
let value: FormValue | number | string = v;
try {
value = (await handleChange(onChange, value)) ?? value;
} catch (error) {
console.error(error);
}
if (
(name || (typeof name === 'number' && name === 0)) &&
formModel.value !== value &&
(v !== value || formModel.value[name] !== value)
) {
formModel.value[name] = value;
}
};
</script>

<template>
<div>
<ElFormItem
:class="{ hidden: `${labelWidth}` === '0' || !config.label }"
:prop="itemProp"
:label-width="labelWidth"
:label="config.label"
>
<component
:key="config.name"
:is="type"
:form-model="formModel"
:config="config"
:name="name"
:prop="itemProp"
@change="onChangeHandler"
></component>
</ElFormItem>
</div>
</template>
./types
98 changes: 98 additions & 0 deletions main/src/form/Form.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
import { provide, reactive, ref, toRaw, toRefs } from 'vue';
import { ElForm, FormInstance } from 'element-plus';
import { FormStateKey } from '../constants/inject-key';
import Container from './Container.vue';
import { FormConfig, FormItemLabelPosition, FormSize, FormState, FormValue } from './types';
const props = withDefaults(
defineProps<{
config?: FormConfig;
initValues?: FormValue;
parentValues?: FormValue;
labelWidth?: string;
disabled?: boolean;
height?: string;
size?: FormSize;
inline?: boolean;
labelPosition?: FormItemLabelPosition;
keyProp?: string;
}>(),
{
config: () => [],
initValues: () => ({}),
parentValues: () => ({}),
labelWidth: '200px',
disabled: false,
height: 'auto',
size: 'default',
inline: false,
labelPosition: 'right',
keyProp: '__key',
}
);
const emits = defineEmits<{
(event: 'change', value: FormValue): void;
}>();
const fields = new Map<string, any>();
const { keyProp, config, initValues, parentValues } = toRefs(props);
const formRef = ref<FormInstance>();
const formModel = ref<FormValue>({});
const formState = reactive<FormState>({
keyProp: keyProp.value,
config: config.value,
initValues: initValues.value,
parentValues: parentValues.value,
formModel: formModel.value,
$emit: emits,
setField(prop, field) {
fields.set(prop, field);
},
getField(prop) {
return fields.get(prop);
},
deleteField(prop) {
return fields.delete(prop);
},
});
const handleChange = () => emits('change', formModel.value);
const handleSubmit = async (): Promise<FormValue> => {
try {
await formRef.value?.validate();
return toRaw(formModel.value);
} catch (invalidFields: any) {
const error: string[] = [];
Object;
throw new Error(error.join('<br>'));
}
};
const handleReset = () => formRef.value?.resetFields();
provide(FormStateKey, formState);
defineExpose({
values: formModel,
formState,
handleChange,
resetForm: handleReset,
submitForm: handleSubmit,
});
</script>

<template>
<ElForm ref="formRef" :model="formModel" :label-position="labelPosition" :size="size">
<Container
v-for="(childConfig, index) in props.config"
:key="index"
:config="childConfig"
:form-model="formModel"
></Container>
</ElForm>
</template>
./types
37 changes: 37 additions & 0 deletions main/src/form/fields/RadioGroup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { computed } from 'vue';
import RadioGroup from '../../components/RadioGroup.vue';
import RadioGroupOption from '../../components/RadioGroupOption.vue';
import { FormValue, RadioGroupConfig } from '../../types';
const props = defineProps<{
config: RadioGroupConfig;
formModel: FormValue;
initValues?: FormValue;
name: string;
prop: string;
}>();
const emits = defineEmits<{
(event: 'change', value: any): void;
}>();
const selectedValue = computed({
get() {
return props.formModel[props.name];
},
set(value) {
emits('change', value);
},
});
</script>

<template>
<RadioGroup v-model="selectedValue">
<RadioGroupOption v-for="(option, index) in config.options" :key="index" :value="option.value">
{{ option.text }}
</RadioGroupOption>
</RadioGroup>
</template>
../types
14 changes: 14 additions & 0 deletions main/src/form/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Plugin } from 'vue';

import RadioGroup from './fields/RadioGroup.vue';

export { default as Form } from './Form.vue';
export * from './types';

export const formPlugin: Plugin = {
install(app) {
app.component('RadioGroup', RadioGroup);
},
};

export default formPlugin;
68 changes: 35 additions & 33 deletions main/src/types.ts → main/src/form/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export type FormValue = Record<string | number, any>;

export type FormSize = 'small' | 'default' | 'large';

export type FormItemLabelPosition = 'top' | 'left' | 'right';

export type FormState = {
config: FormConfig;
popperClass?: string;
initValues: FormValue;
lastValues: FormValue;
isCompare: boolean;
values: FormValue;
$emit: (event: string, ...args: any[]) => void;
formModel: FormValue;
$emit: (event: any, ...args: any[]) => void;
keyProp?: string;
parentValues?: FormValue;
setField: (prop: string, field: any) => void;
Expand All @@ -16,7 +17,19 @@ export type FormState = {
[key: string]: any;
};

export type RuleValidator = (
export interface FormHandlerData {
/** 表单的初始值 */
initValue: FormValue;
/** 当前作用域下的值 */
model: FormValue;
parent?: FormValue;
/** 整个表单的值 */
formValue: FormValue;
prop: string | number;
config: any;
}

export type RuleValidatorHandler = (
options: {
rule: string;
value: any;
Expand All @@ -26,59 +39,48 @@ export type RuleValidator = (
messages: string;
};
},
data: {
/** 表单的初始值 */
values: FormValue;
/** 当前作用域下的值 */
model: FormValue;
parent: FormValue;
/** 整个表单的值 */
formValue: FormValue;
prop: string;
config: any;
},
data: FormHandlerData,
formState: FormState | undefined
) => void;

export type TypeFunction = (
mForm: FormState | undefined,
data: {
model: FormValue;
}
) => string;

export interface Rule {
message?: string;
/** 系统提供的验证器类型。有:string,number,boolean,method,regexp,integer,float,array,object,enum,date,url,hex,email,any */
type?: string;
/** 是否必填 */
required?: boolean;
/** 自定义验证器 */
validator?: RuleValidator;
validator?: RuleValidatorHandler;
}

type OnChangeHandler = (
formState: FormState | undefined,
value: any,
data: {
model: FormValue;
values: FormValue;
parent?: FormValue;
formValue: FormValue;
config: FormConfig;
}
) => any;
export type OnChangeHandler = (formState: FormState | undefined, value: any, data: FormHandlerData) => any;

export interface FormItem {
type: string;
type: string | TypeFunction;
name?: string;
label?: string;
onChange?: OnChangeHandler;
}

export interface RadioGroupConfig extends FormItem {
type: 'radioGroup';
type: 'RadioGroup';
options: {
value: string | number | boolean;
text: string;
}[];
}

export interface ColorPickConfig extends FormItem {
type: 'colorPicker';
type: 'ColorPicker';
}

export type FormConfig = FormItem | RadioGroupConfig | ColorPickConfig;
export type ChildConfig = FormItem | RadioGroupConfig | ColorPickConfig;

export type FormConfig = ChildConfig[];
Loading

0 comments on commit 9f8bfb4

Please sign in to comment.