Skip to content

Commit

Permalink
feat(core): support interpolation expression
Browse files Browse the repository at this point in the history
  • Loading branch information
Muluk-m committed Sep 19, 2022
1 parent 5799f91 commit 44f06fa
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 44 deletions.
4 changes: 2 additions & 2 deletions packages/vue3-example/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue';
import { Button } from 'vant';
import SchemaForm, { FormRef } from '../../vue3-schema-form/src';
import SchemaForm, { FormRef, Schema } from '../../vue3-schema-form/src';
import Test from './widgets/Test.vue';
const formRef = ref<FormRef>();
Expand Down Expand Up @@ -79,7 +79,7 @@ const schema = {
},
},
},
};
} as unknown as Schema;
</script>

<template>
Expand Down
77 changes: 44 additions & 33 deletions packages/vue3-schema-form/src/core/SchemaForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { defineComponent, computed, provide, watch, unref, ref } from 'vue';
import { defineComponent, computed, watch, unref, ref } from 'vue';
import { useChildren } from '../hooks/useRelation';
import { createNamespace } from '../utils';

import { SFPropsKey, SFDataKey, SFRelationKey } from '../constants';
import { SFRelationKey } from '../constants';
import { schemaFormProps, ErrorMessage } from '../types';
import { validateAll, validateSingle } from './validator';
import { getFieldConfigs, handleRemoveHiddenData } from './handleField';
import { handleRemoveHiddenData } from './handleField';
import { createSchemaCore } from './createCore';
import FieldItem from './Field';
import './index.scss';

Expand All @@ -22,41 +23,51 @@ export default defineComponent({
emits: ['update:modelValue'],

setup: (props, { emit, expose }) => {
const fieldConfigList = computed(() => getFieldConfigs(props));
const schemaCore = computed(() => createSchemaCore(props));
const schemaRenderer = computed(() =>
schemaCore.value?.renderer({
validate,
validateFields,
getFormData,
setFormData,
})
);
const errorFields = ref({});
const { children, linkChildren } = useChildren(SFRelationKey);
const formData = computed({
get: () => unref(props.modelValue),
set: (value: unknown) => {
emit('update:modelValue', value);
},
});

linkChildren({
props: computed(() => props),
formData: computed({
get: () => unref(props.modelValue),
set: (value: unknown) => {
emit('update:modelValue', value);
},
}),
formData,
});

provide(
SFPropsKey,
computed(() => props)
);
provide(
SFDataKey,
computed({
get: () => unref(props.modelValue),
set: (value: any) => {
emit('update:modelValue', value);
},
})
);

const getFilteredFormData = () =>
handleRemoveHiddenData(unref(props.modelValue), fieldConfigList.value);
handleRemoveHiddenData(
unref(props.modelValue),
schemaRenderer.value?.map(({ name }) => name) ?? []
);

/** 获取表单值,如果配置removeHiddenData 则过滤掉hidden字段 */
const getFormData = () =>
props.removeHiddenData ? getFilteredFormData() : unref(props.modelValue);

const setValueByName = (name: string, value: unknown) => {
if (formData) {
formData[name] = value;
}
};

const setFormData = (values: Partial<FormData>) => {
for (const [key, value] of Object.entries(values)) {
setValueByName(key, value);
}
};

/** 视口滚动到指定字段 */
const scrollToField = (
name: string,
Expand All @@ -80,7 +91,7 @@ export default defineComponent({
const validateField = (fieldName: string, scrollToError = true) => {
const formData = getFilteredFormData();
const fieldData = formData[fieldName];
const fieldSchema = props.schema.properties?.[fieldName];
const fieldSchema = schemaCore.value?.schema.properties?.[fieldName];

if (fieldData && fieldSchema) {
return validateSingle(fieldData, fieldSchema, fieldName).then((errors) => {
Expand Down Expand Up @@ -116,7 +127,7 @@ export default defineComponent({
const validate = (scrollToError = true) =>
validateAll({
formData: getFilteredFormData(),
descriptor: props.schema.properties!,
descriptor: schemaCore.value?.schema.properties,
}).then((errors) => {
// scroll to error position
if (scrollToError && errors.length) {
Expand Down Expand Up @@ -179,7 +190,7 @@ export default defineComponent({
if (props.debug && process.env.NODE_ENV !== 'production') {
console.group('Action');
console.log('%cNext:', 'color: #47B04B; font-weight: 700;', value);
console.log('%cConfig:', 'color: #1E80FF; font-weight: 700;', fieldConfigList.value);
console.log('%cConfig:', 'color: #1E80FF; font-weight: 700;', schemaRenderer.value);
console.groupEnd();
}
});
Expand All @@ -192,12 +203,12 @@ export default defineComponent({

return () => (
<div class={name}>
{fieldConfigList.value.map((config) => (
{schemaRenderer.value?.map((scoped) => (
<FieldItem
name={config.name}
key={config.name}
addon={{ ...config, validate, validateFields, getFormData }}
errorMessage={errorFields.value[config.name] ?? ''}
name={scoped.name}
key={scoped.name}
addon={scoped}
errorMessage={errorFields.value[scoped.name] ?? ''}
/>
))}
</div>
Expand Down
2 changes: 2 additions & 0 deletions packages/vue3-schema-form/src/types/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export type ErrorMessage = {
name: string;
error: string[];
};

export type Deps = Record<string, any>;
5 changes: 5 additions & 0 deletions packages/vue3-schema-form/src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export const schemaFormProps = {
type: Object as PropType<Widgets>,
default: () => ({}),
},
/** 依赖的外部状态,用于插值表达式中的 $deps */
deps: {
type: Object,
default: () => ({}),
},
/** 只读模式 */
readonly: Boolean,
/** 禁用模式 */
Expand Down
17 changes: 8 additions & 9 deletions packages/vue3-schema-form/src/types/schema.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
import { RuleItem } from 'async-validator';
import { FormData } from '.';

export type ValueType = 'string' | 'object' | 'array' | 'number' | 'boolean' | 'date' | string;

export type PayloadBoolean = boolean | ((data: FormData) => boolean);

export type PayloadString = string | ((data: FormData) => string);

export interface SchemaBase {
type: ValueType;
title: string;
/** 是否必填,支持函数表达式 (formData)=> boolean */
required: PayloadBoolean;
required: boolean;
placeholder: string;
/** 改变字段绑定值 用户并不希望纯展示的字段也出现在表单中,此时,使用 bind: false 可避免字段在提交时出现 */
// bind: false | string | string[];
/** 是否禁用,支持函数表达式 (formData)=> boolean */
disabled: PayloadBoolean;
disabled: boolean;
/** 是否只读,支持函数表达式 (formData)=> boolean */
readonly: PayloadBoolean;
readonly: boolean;
/** 是否隐藏,隐藏的字段不会在 formData 里透出,支持函数表达式 (formData)=> boolean */
hidden: PayloadBoolean;
hidden: boolean;
/** Label 与 Field 的展示关系,row 表示并排展示,column 表示两排展示 */
displayType: 'row' | 'column' | string;
/** label宽度 !暂时不支持,如果有场景需要,可以考虑支持 */
Expand All @@ -40,4 +35,8 @@ export interface SchemaBase {
border: boolean;
}

export type SchemaSegments<T = SchemaBase> = {
[P in keyof T]?: T[P] extends boolean | number ? T[P] | string : T[P];
};

export type Schema = Partial<SchemaBase>;
1 change: 1 addition & 0 deletions packages/vue3-schema-form/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './validate';
export * from './basic';
export * from './crate';
export * from './widget';
export * from './resolver';
76 changes: 76 additions & 0 deletions packages/vue3-schema-form/src/utils/resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { FormData, Deps, Schema } from '../types';
import { isJsonSchema, isSegment, isObject } from './validate';

/* eslint-disable no-new-func */
export const safeEval = (code: string) => {
return Function(`"use strict"; ${code}`)();
};

/**
* 解析表达式计算结果
*/
export const evaluateSegment = (
segment: string,
selfValue: unknown,
formData: FormData,
deps: Deps
) => {
try {
return safeEval(`
const $selfValue =${JSON.stringify(selfValue)};
const $values = ${JSON.stringify(formData)};
const $deps = ${JSON.stringify(deps)};
return (${segment})
`);
} catch (error) {
console.error(error, 'expression:', segment, 'formData:', formData);
return null;
}
};

export const resolvePropertiesSegment = (
properties: Record<string, any>,
formData: FormData,
deps: Deps
): Record<string, any> => {
const resolvedProperties = {};

Object.keys(properties).forEach((key) => {
const val = properties[key];
if (Array.isArray(val)) {
resolvedProperties[key] = val;
} else if (isObject(val)) {
resolvedProperties[key] = resolvePropertiesSegment(val, formData, deps);
} else if (isSegment(val)) {
resolvedProperties[key] = evaluateSegment(val.slice(2, -2), formData[key], formData, deps);
} else {
resolvedProperties[key] = val;
}
});

return resolvedProperties;
};

export const resolveSchemaWithSegments = (
schema: Schema,
formData: FormData,
deps: Deps
): Schema => {
if (!isJsonSchema(schema)) {
return schema;
}

const { properties = {}, ...rest } = schema;

const resolvedProperties = Object.fromEntries(
Object.entries(properties).map(([field, property]) => [
field,
resolvePropertiesSegment(property, formData, deps),
])
);

return {
...rest,
properties: resolvedProperties,
};
};
13 changes: 13 additions & 0 deletions packages/vue3-schema-form/src/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Numeric } from './basic';
import { Schema } from '../types';

export const isDef = <T>(val: T): val is NonNullable<T> => val !== undefined && val !== null;

Expand Down Expand Up @@ -32,6 +33,18 @@ export const isEmptyValue = (value: unknown) => {
return !value;
};

export const isJsonSchema = (schema: Record<string, unknown>): schema is Schema =>
schema.properties !== undefined && schema.type === 'object';

export const isSegment = (segment: string): segment is string => {
const pattern = /^{{(.+)}}$/;
if (typeof segment === 'string' && pattern.test(segment)) {
return true;
}

return false;
};

export const isJSON = (str: string) => {
try {
if (typeof JSON.parse(str) === 'object') {
Expand Down

0 comments on commit 44f06fa

Please sign in to comment.