525 lines
14 KiB
TypeScript
525 lines
14 KiB
TypeScript
import difference from 'lodash/difference';
|
||
import omit from 'lodash/omit';
|
||
import React from 'react';
|
||
import LazyComponent from './components/LazyComponent';
|
||
import {
|
||
filterSchema,
|
||
loadRenderer,
|
||
RendererConfig,
|
||
RendererEnv,
|
||
RendererProps,
|
||
resolveRenderer
|
||
} from './factory';
|
||
import {asFormItem} from './renderers/Item';
|
||
import {IScopedContext, ScopedContext} from './Scoped';
|
||
import {Schema, SchemaNode} from './types';
|
||
import {DebugWrapper} from './utils/debug';
|
||
import getExprProperties from './utils/filter-schema';
|
||
import {anyChanged, chainEvents, autobind} from './utils/helper';
|
||
import {SimpleMap} from './utils/SimpleMap';
|
||
import {bindEvent, dispatchEvent, RendererEvent} from './utils/renderer-event';
|
||
import {isAlive} from 'mobx-state-tree';
|
||
import {reaction} from 'mobx';
|
||
import {resolveVariableAndFilter} from './utils/tpl-builtin';
|
||
import {buildStyle} from './utils/style';
|
||
import {StatusScopedProps} from './StatusScoped';
|
||
import {evalExpression, filter} from './utils/tpl';
|
||
|
||
interface SchemaRendererProps
|
||
extends Partial<Omit<RendererProps, 'statusStore'>>,
|
||
StatusScopedProps {
|
||
schema: Schema;
|
||
$path: string;
|
||
env: RendererEnv;
|
||
}
|
||
|
||
export const RENDERER_TRANSMISSION_OMIT_PROPS = [
|
||
'type',
|
||
'name',
|
||
'$ref',
|
||
'className',
|
||
'style',
|
||
'data',
|
||
'children',
|
||
'ref',
|
||
'visible',
|
||
'visibleOn',
|
||
'hidden',
|
||
'hiddenOn',
|
||
'disabled',
|
||
'disabledOn',
|
||
'static',
|
||
'staticOn',
|
||
'component',
|
||
'detectField',
|
||
'defaultValue',
|
||
'defaultData',
|
||
'required',
|
||
'requiredOn',
|
||
'syncSuperStore',
|
||
'mode',
|
||
'body',
|
||
'id',
|
||
'inputOnly',
|
||
'label',
|
||
'renderLabel',
|
||
'trackExpression',
|
||
'editorSetting',
|
||
'updatePristineAfterStoreDataReInit'
|
||
];
|
||
|
||
const componentCache: SimpleMap = new SimpleMap();
|
||
|
||
export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||
static displayName: string = 'Renderer';
|
||
static contextType = ScopedContext;
|
||
|
||
rendererKey = '';
|
||
renderer: RendererConfig | null;
|
||
ref: any;
|
||
cRef: any;
|
||
|
||
schema: any;
|
||
path: string;
|
||
|
||
reaction: any;
|
||
unbindEvent: (() => void) | undefined = undefined;
|
||
isStatic: any = undefined;
|
||
|
||
constructor(props: SchemaRendererProps) {
|
||
super(props);
|
||
this.refFn = this.refFn.bind(this);
|
||
this.renderChild = this.renderChild.bind(this);
|
||
this.reRender = this.reRender.bind(this);
|
||
this.resolveRenderer(this.props);
|
||
this.dispatchEvent = this.dispatchEvent.bind(this);
|
||
|
||
// 监听statusStore更新
|
||
this.reaction = reaction(
|
||
() => {
|
||
const id = filter(props.schema.id, props.data);
|
||
const name = filter(props.schema.name, props.data);
|
||
return `${
|
||
props.statusStore.visibleState[id] ??
|
||
props.statusStore.visibleState[name]
|
||
}${
|
||
props.statusStore.disableState[id] ??
|
||
props.statusStore.disableState[name]
|
||
}${
|
||
props.statusStore.staticState[id] ??
|
||
props.statusStore.staticState[name]
|
||
}`;
|
||
},
|
||
() => this.forceUpdate()
|
||
);
|
||
}
|
||
|
||
componentDidMount() {
|
||
// 这里无法区分监听的是不是广播,所以又bind一下,主要是为了绑广播
|
||
this.unbindEvent = bindEvent(this.cRef);
|
||
}
|
||
|
||
componentWillUnmount() {
|
||
this.reaction?.();
|
||
this.unbindEvent?.();
|
||
}
|
||
|
||
// 限制:只有 schema 除外的 props 变化,或者 schema 里面的某个成员值发生变化才更新。
|
||
shouldComponentUpdate(nextProps: SchemaRendererProps) {
|
||
const props = this.props;
|
||
const list: Array<string> = difference(Object.keys(nextProps), [
|
||
'schema',
|
||
'scope'
|
||
]);
|
||
|
||
if (
|
||
difference(Object.keys(props), ['schema', 'scope']).length !==
|
||
list.length ||
|
||
anyChanged(list, this.props, nextProps)
|
||
) {
|
||
return true;
|
||
} else {
|
||
const list: Array<string> = Object.keys(nextProps.schema);
|
||
|
||
if (
|
||
Object.keys(props.schema).length !== list.length ||
|
||
anyChanged(list, props.schema, nextProps.schema)
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
resolveRenderer(props: SchemaRendererProps, force = false): any {
|
||
let schema = props.schema;
|
||
let path = props.$path;
|
||
|
||
if (schema && schema.$ref) {
|
||
schema = {
|
||
...props.resolveDefinitions(schema.$ref),
|
||
...schema
|
||
};
|
||
|
||
path = path.replace(/(?!.*\/).*/, schema.type);
|
||
}
|
||
|
||
if (
|
||
schema?.type &&
|
||
(force ||
|
||
!this.renderer ||
|
||
this.rendererKey !== `${schema.type}-${schema.$$id}`)
|
||
) {
|
||
const rendererResolver = props.env.rendererResolver || resolveRenderer;
|
||
this.renderer = rendererResolver(path, schema, props);
|
||
this.rendererKey = `${schema.type}-${schema.$$id}`;
|
||
} else {
|
||
// 自定义组件如果在节点设置了 label name 什么的,就用 formItem 包一层
|
||
// 至少自动支持了 valdiations, label, description 等逻辑。
|
||
if (schema.children && !schema.component && schema.asFormItem) {
|
||
schema.component = PlaceholderComponent;
|
||
schema.renderChildren = schema.children;
|
||
delete schema.children;
|
||
}
|
||
|
||
if (
|
||
schema.component &&
|
||
!schema.component.wrapedAsFormItem &&
|
||
schema.asFormItem
|
||
) {
|
||
const cache = componentCache.get(schema.component);
|
||
|
||
if (cache) {
|
||
schema.component = cache;
|
||
} else {
|
||
const cache = asFormItem({
|
||
strictMode: false,
|
||
...schema.asFormItem
|
||
})(schema.component);
|
||
componentCache.set(schema.component, cache);
|
||
cache.wrapedAsFormItem = true;
|
||
schema.component = cache;
|
||
}
|
||
}
|
||
}
|
||
|
||
return {path, schema};
|
||
}
|
||
|
||
getWrappedInstance() {
|
||
return this.cRef;
|
||
}
|
||
|
||
refFn(ref: any) {
|
||
this.ref = ref;
|
||
}
|
||
|
||
@autobind
|
||
childRef(ref: any) {
|
||
while (ref && ref.getWrappedInstance) {
|
||
ref = ref.getWrappedInstance();
|
||
}
|
||
|
||
this.cRef = ref;
|
||
}
|
||
|
||
async dispatchEvent(
|
||
e: React.MouseEvent<any>,
|
||
data: any,
|
||
renderer?: React.Component<RendererProps> // for didmount
|
||
): Promise<RendererEvent<any> | void> {
|
||
return await dispatchEvent(
|
||
e,
|
||
this.cRef || renderer,
|
||
this.context as IScopedContext,
|
||
data
|
||
);
|
||
}
|
||
|
||
renderChild(
|
||
region: string,
|
||
node?: SchemaNode,
|
||
subProps: {
|
||
data?: object;
|
||
[propName: string]: any;
|
||
} = {}
|
||
) {
|
||
let {schema: _, $path: __, env, render, ...rest} = this.props;
|
||
let {path: $path} = this.resolveRenderer(this.props);
|
||
|
||
const omitList = RENDERER_TRANSMISSION_OMIT_PROPS.concat();
|
||
if (this.renderer) {
|
||
const Component = this.renderer.component;
|
||
Component.propsList &&
|
||
omitList.push.apply(omitList, Component.propsList as Array<string>);
|
||
}
|
||
|
||
return render!(`${$path}${region ? `/${region}` : ''}`, node || '', {
|
||
...omit(rest, omitList),
|
||
defaultStatic:
|
||
(this.renderer?.type &&
|
||
['drawer', 'dialog'].includes(this.renderer.type)
|
||
? false
|
||
: undefined) ??
|
||
this.isStatic ??
|
||
(_.staticOn
|
||
? evalExpression(_.staticOn, rest.data)
|
||
: _.static ?? rest.defaultStatic),
|
||
...subProps,
|
||
data: subProps.data || rest.data,
|
||
env: env
|
||
});
|
||
}
|
||
|
||
reRender() {
|
||
this.resolveRenderer(this.props, true);
|
||
this.forceUpdate();
|
||
}
|
||
|
||
render(): JSX.Element | null {
|
||
let {
|
||
$path: _,
|
||
schema: __,
|
||
rootStore,
|
||
statusStore,
|
||
render,
|
||
...rest
|
||
} = this.props;
|
||
|
||
if (__ == null) {
|
||
return null;
|
||
}
|
||
|
||
let {path: $path, schema} = this.resolveRenderer(this.props);
|
||
const theme = this.props.env.theme;
|
||
|
||
if (Array.isArray(schema)) {
|
||
return render!($path, schema as any, rest) as JSX.Element;
|
||
}
|
||
|
||
const detectData =
|
||
schema &&
|
||
(schema.detectField === '&' ? rest : rest[schema.detectField || 'data']);
|
||
let exprProps: any = detectData
|
||
? getExprProperties(schema, detectData, undefined, rest)
|
||
: {};
|
||
|
||
// 控制显隐
|
||
const id = filter(schema.id, rest.data);
|
||
const name = filter(schema.name, rest.data);
|
||
const visible = isAlive(statusStore)
|
||
? statusStore.visibleState[id] ?? statusStore.visibleState[name]
|
||
: undefined;
|
||
const disable = isAlive(statusStore)
|
||
? statusStore.disableState[id] ?? statusStore.disableState[name]
|
||
: undefined;
|
||
const isStatic = isAlive(statusStore)
|
||
? statusStore.staticState[id] ?? statusStore.staticState[name]
|
||
: undefined;
|
||
this.isStatic = isStatic;
|
||
|
||
if (
|
||
visible === false ||
|
||
(visible !== true &&
|
||
exprProps &&
|
||
(exprProps.hidden ||
|
||
exprProps.visible === false ||
|
||
schema.hidden ||
|
||
schema.visible === false ||
|
||
rest.hidden ||
|
||
rest.visible === false))
|
||
) {
|
||
(rest as any).invisible = true;
|
||
}
|
||
|
||
if (schema.children) {
|
||
return rest.invisible
|
||
? null
|
||
: React.isValidElement(schema.children)
|
||
? schema.children
|
||
: (schema.children as Function)({
|
||
...rest,
|
||
...exprProps,
|
||
$path: $path,
|
||
$schema: schema,
|
||
render: this.renderChild,
|
||
forwardedRef: this.refFn,
|
||
rootStore,
|
||
statusStore,
|
||
dispatchEvent: this.dispatchEvent
|
||
});
|
||
} else if (typeof schema.component === 'function') {
|
||
const isSFC = !(schema.component.prototype instanceof React.Component);
|
||
const {
|
||
data: defaultData,
|
||
value: defaultValue, // render时的value改放defaultValue中
|
||
activeKey: defaultActiveKey,
|
||
key: propKey,
|
||
...restSchema
|
||
} = schema;
|
||
return rest.invisible
|
||
? null
|
||
: React.createElement(schema.component as any, {
|
||
...rest,
|
||
...restSchema,
|
||
...exprProps,
|
||
// value: defaultValue, // 备注: 此处并没有将value传递给渲染器
|
||
defaultData,
|
||
defaultValue,
|
||
defaultActiveKey,
|
||
propKey,
|
||
$path: $path,
|
||
$schema: schema,
|
||
ref: isSFC ? undefined : this.refFn,
|
||
forwardedRef: isSFC ? this.refFn : undefined,
|
||
render: this.renderChild,
|
||
rootStore,
|
||
statusStore,
|
||
dispatchEvent: this.dispatchEvent
|
||
});
|
||
} else if (Object.keys(schema).length === 0) {
|
||
return null;
|
||
} else if (!this.renderer) {
|
||
return rest.invisible ? null : (
|
||
<LazyComponent
|
||
{...rest}
|
||
{...exprProps}
|
||
getComponent={async () => {
|
||
const result = await rest.env.loadRenderer(
|
||
schema,
|
||
$path,
|
||
this.reRender
|
||
);
|
||
if (result && typeof result === 'function') {
|
||
return result;
|
||
} else if (result && React.isValidElement(result)) {
|
||
return () => result;
|
||
}
|
||
|
||
this.reRender();
|
||
return () => loadRenderer(schema, $path);
|
||
}}
|
||
$path={$path}
|
||
$schema={schema}
|
||
retry={this.reRender}
|
||
rootStore={rootStore}
|
||
statusStore={statusStore}
|
||
dispatchEvent={this.dispatchEvent}
|
||
/>
|
||
);
|
||
}
|
||
|
||
const renderer = this.renderer as RendererConfig;
|
||
schema = filterSchema(schema, renderer, rest);
|
||
const {
|
||
data: defaultData,
|
||
value: defaultValue,
|
||
key: propKey,
|
||
activeKey: defaultActiveKey,
|
||
...restSchema
|
||
} = schema;
|
||
const Component = renderer.component;
|
||
|
||
// 原来表单项的 visible: false 和 hidden: true 表单项的值和验证是有效的
|
||
// 而 visibleOn 和 hiddenOn 是无效的,
|
||
// 这个本来就是个bug,但是已经被广泛使用了
|
||
// 我只能继续实现这个bug了
|
||
if (
|
||
rest.invisible &&
|
||
(exprProps.hidden ||
|
||
exprProps.visible === false ||
|
||
!renderer.isFormItem ||
|
||
(schema.visible !== false && !schema.hidden))
|
||
) {
|
||
return null;
|
||
}
|
||
|
||
// withStore 里面会处理,而且会实时处理
|
||
// 这里处理反而导致了问题
|
||
if (renderer.storeType) {
|
||
exprProps = {};
|
||
}
|
||
|
||
const isClassComponent = Component.prototype?.isReactComponent;
|
||
let props = {
|
||
...theme.getRendererConfig(renderer.name),
|
||
...restSchema,
|
||
...chainEvents(rest, restSchema),
|
||
...exprProps,
|
||
// value: defaultValue, // 备注: 此处并没有将value传递给渲染器
|
||
defaultData: restSchema.defaultData ?? defaultData,
|
||
defaultValue: restSchema.defaultValue ?? defaultValue,
|
||
defaultActiveKey: defaultActiveKey,
|
||
propKey: propKey,
|
||
$path: $path,
|
||
$schema: schema,
|
||
ref: this.refFn,
|
||
render: this.renderChild,
|
||
rootStore,
|
||
statusStore,
|
||
dispatchEvent: this.dispatchEvent,
|
||
mobileUI: schema.useMobileUI === false ? false : rest.mobileUI
|
||
};
|
||
|
||
// style 支持公式
|
||
if (schema.style) {
|
||
(props as any).style = buildStyle(schema.style, detectData);
|
||
}
|
||
|
||
if (disable !== undefined) {
|
||
(props as any).disabled = disable;
|
||
}
|
||
|
||
if (isStatic !== undefined) {
|
||
(props as any).static = isStatic;
|
||
}
|
||
|
||
// 自动解析变量模式,主要是方便直接引入第三方组件库,无需为了支持变量封装一层
|
||
if (renderer.autoVar) {
|
||
for (const key of Object.keys(schema)) {
|
||
if (typeof props[key] === 'string') {
|
||
props[key] = resolveVariableAndFilter(
|
||
props[key],
|
||
props.data,
|
||
'| raw'
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
const component = isClassComponent ? (
|
||
<Component {...props} ref={this.childRef} />
|
||
) : (
|
||
<Component {...props} />
|
||
);
|
||
|
||
return this.props.env.enableAMISDebug ? (
|
||
<DebugWrapper renderer={renderer}>{component}</DebugWrapper>
|
||
) : (
|
||
component
|
||
);
|
||
}
|
||
}
|
||
|
||
class PlaceholderComponent extends React.Component {
|
||
childRef = React.createRef<any>();
|
||
|
||
getWrappedInstance() {
|
||
return this.childRef.current;
|
||
}
|
||
|
||
render() {
|
||
const {renderChildren, ...rest} = this.props as any;
|
||
|
||
if (typeof renderChildren === 'function') {
|
||
return renderChildren({
|
||
...rest,
|
||
ref: this.childRef
|
||
});
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|