mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
parent
be33bf98d2
commit
2df39a9c68
@ -153,7 +153,8 @@ fis.match('{*.ts,*.jsx,*.tsx,/src/**.js,/src/**.ts}', {
|
||||
importHelpers: true,
|
||||
esModuleInterop: true,
|
||||
experimentalDecorators: true,
|
||||
sourceMap: true
|
||||
sourceMap: true,
|
||||
target: 4
|
||||
}),
|
||||
|
||||
function (content) {
|
||||
|
171
src/Root.tsx
Normal file
171
src/Root.tsx
Normal file
@ -0,0 +1,171 @@
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import qs from 'qs';
|
||||
import React from 'react';
|
||||
import Alert from './components/Alert2';
|
||||
import ImageGallery from './components/ImageGallery';
|
||||
import {envOverwrite} from './envOverwrite';
|
||||
import {RendererEnv, RendererProps} from './factory';
|
||||
import {LocaleContext, TranslateFn} from './locale';
|
||||
import {RootRenderer} from './RootRenderer';
|
||||
import {SchemaRenderer} from './SchemaRenderer';
|
||||
import Scoped from './Scoped';
|
||||
import {IRendererStore} from './store';
|
||||
import {ThemeContext} from './theme';
|
||||
import {Schema, SchemaNode} from './types';
|
||||
import getExprProperties from './utils/filter-schema';
|
||||
import {autobind, createObject, isEmpty} from './utils/helper';
|
||||
import {RootStoreContext} from './WithRootStore';
|
||||
|
||||
export interface RootRenderProps {
|
||||
location?: Location;
|
||||
theme?: string;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface RootProps {
|
||||
schema: SchemaNode;
|
||||
rootStore: IRendererStore;
|
||||
env: RendererEnv;
|
||||
theme: string;
|
||||
pathPrefix?: string;
|
||||
locale?: string;
|
||||
translate?: TranslateFn;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export class Root extends React.Component<RootProps> {
|
||||
@autobind
|
||||
resolveDefinitions(name: string) {
|
||||
const definitions = (this.props.schema as Schema).definitions;
|
||||
if (!name || isEmpty(definitions)) {
|
||||
return {};
|
||||
}
|
||||
return definitions && definitions[name];
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
schema,
|
||||
rootStore,
|
||||
env,
|
||||
pathPrefix,
|
||||
location,
|
||||
data,
|
||||
locale,
|
||||
translate,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const theme = env.theme;
|
||||
|
||||
// 根据环境覆盖 schema,这个要在最前面做,不然就无法覆盖 validations
|
||||
envOverwrite(schema, locale);
|
||||
|
||||
return (
|
||||
<RootStoreContext.Provider value={rootStore}>
|
||||
<ThemeContext.Provider value={this.props.theme || 'default'}>
|
||||
<LocaleContext.Provider value={this.props.locale!}>
|
||||
<ImageGallery modalContainer={env.getModalContainer}>
|
||||
<RootRenderer
|
||||
pathPrefix={pathPrefix || ''}
|
||||
schema={
|
||||
isPlainObject(schema)
|
||||
? {
|
||||
type: 'page',
|
||||
...(schema as any)
|
||||
}
|
||||
: schema
|
||||
}
|
||||
{...rest}
|
||||
rootStore={rootStore}
|
||||
resolveDefinitions={this.resolveDefinitions}
|
||||
location={location}
|
||||
data={data}
|
||||
env={env}
|
||||
classnames={theme.classnames}
|
||||
classPrefix={theme.classPrefix}
|
||||
locale={locale}
|
||||
translate={translate}
|
||||
/>
|
||||
</ImageGallery>
|
||||
</LocaleContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
</RootStoreContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface renderChildProps extends Partial<RendererProps> {
|
||||
env: RendererEnv;
|
||||
}
|
||||
export type ReactElement = React.ReactNode[] | JSX.Element | null | false;
|
||||
|
||||
export function renderChildren(
|
||||
prefix: string,
|
||||
node: SchemaNode,
|
||||
props: renderChildProps
|
||||
): ReactElement {
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((node, index) =>
|
||||
renderChild(`${prefix}/${index}`, node, {
|
||||
...props,
|
||||
key: `${props.key ? `${props.key}-` : ''}${index}`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return renderChild(prefix, node, props);
|
||||
}
|
||||
|
||||
export function renderChild(
|
||||
prefix: string,
|
||||
node: SchemaNode,
|
||||
props: renderChildProps
|
||||
): ReactElement {
|
||||
if (Array.isArray(node)) {
|
||||
return renderChildren(prefix, node, props);
|
||||
}
|
||||
|
||||
const typeofnode = typeof node;
|
||||
let schema: Schema =
|
||||
typeofnode === 'string' || typeofnode === 'number'
|
||||
? {type: 'tpl', tpl: String(node)}
|
||||
: (node as Schema);
|
||||
const detectData =
|
||||
schema &&
|
||||
(schema.detectField === '&' ? props : props[schema.detectField || 'data']);
|
||||
const exprProps = detectData
|
||||
? getExprProperties(schema, detectData, undefined, props)
|
||||
: null;
|
||||
|
||||
if (
|
||||
exprProps &&
|
||||
(exprProps.hidden ||
|
||||
exprProps.visible === false ||
|
||||
schema.hidden ||
|
||||
schema.visible === false ||
|
||||
props.hidden ||
|
||||
props.visible === false)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transform = props.propsTransform;
|
||||
|
||||
if (transform) {
|
||||
// @ts-ignore
|
||||
delete props.propsTransform;
|
||||
props = transform(props);
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaRenderer
|
||||
{...props}
|
||||
{...exprProps}
|
||||
schema={schema}
|
||||
$path={`${prefix ? `${prefix}/` : ''}${(schema && schema.type) || ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default Scoped(Root);
|
276
src/RootRenderer.tsx
Normal file
276
src/RootRenderer.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
import {observer} from 'mobx-react';
|
||||
import React from 'react';
|
||||
import Alert from './components/Alert2';
|
||||
import Spinner from './components/Spinner';
|
||||
import {renderChild, RootProps} from './Root';
|
||||
import {IScopedContext, ScopedContext} from './Scoped';
|
||||
import {IRootStore, RootStore} from './store/root';
|
||||
import {Action} from './types';
|
||||
import {bulkBindFunctions, guid, isVisible} from './utils/helper';
|
||||
import {filter} from './utils/tpl';
|
||||
|
||||
export interface RootRendererProps extends RootProps {
|
||||
location?: any;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RootRenderer extends React.Component<RootRendererProps> {
|
||||
store: IRootStore;
|
||||
static contextType = ScopedContext;
|
||||
|
||||
constructor(props: RootRendererProps) {
|
||||
super(props);
|
||||
|
||||
this.store = props.rootStore.addStore({
|
||||
id: guid(),
|
||||
path: this.props.$path,
|
||||
storeType: RootStore.name,
|
||||
parentId: ''
|
||||
}) as IRootStore;
|
||||
|
||||
this.store.initData(props.data);
|
||||
this.store.updateLocation(props.location);
|
||||
|
||||
bulkBindFunctions<RootRenderer /*为毛 this 的类型自动识别不出来?*/>(this, [
|
||||
'handleAction',
|
||||
'handleDialogConfirm',
|
||||
'handleDialogClose',
|
||||
'handleDrawerConfirm',
|
||||
'handleDrawerClose'
|
||||
]);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: RootRendererProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (props.data !== prevProps.data) {
|
||||
this.store.updateData(props.data);
|
||||
}
|
||||
|
||||
if (props.location !== prevProps.location) {
|
||||
this.store.updateLocation(props.location);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
this.store.setRuntimeError(error, errorInfo);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.rootStore.removeStore(this.store);
|
||||
}
|
||||
|
||||
handleAction(
|
||||
e: React.UIEvent<any> | void,
|
||||
action: Action,
|
||||
ctx: object,
|
||||
throwErrors: boolean = false,
|
||||
delegate?: IScopedContext
|
||||
) {
|
||||
const {env, messages, onAction} = this.props;
|
||||
const store = this.store;
|
||||
|
||||
if (
|
||||
onAction?.(e, action, ctx, throwErrors, delegate || this.context) ===
|
||||
false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
action.actionType === 'url' ||
|
||||
action.actionType === 'link' ||
|
||||
action.actionType === 'jump'
|
||||
) {
|
||||
if (!env || !env.jumpTo) {
|
||||
throw new Error('env.jumpTo is required!');
|
||||
}
|
||||
|
||||
env.jumpTo(
|
||||
filter(
|
||||
(action.to || action.url || action.link) as string,
|
||||
ctx,
|
||||
'| raw'
|
||||
),
|
||||
action,
|
||||
ctx
|
||||
);
|
||||
} else if (action.actionType === 'dialog') {
|
||||
store.setCurrentAction(action);
|
||||
store.openDialog(ctx);
|
||||
} else if (action.actionType === 'drawer') {
|
||||
store.setCurrentAction(action);
|
||||
store.openDrawer(ctx);
|
||||
} else if (action.actionType === 'ajax') {
|
||||
store.setCurrentAction(action);
|
||||
store
|
||||
.saveRemote(action.api as string, ctx, {
|
||||
successMessage:
|
||||
(action.messages && action.messages.success) ||
|
||||
(messages && messages.saveSuccess),
|
||||
errorMessage:
|
||||
(action.messages && action.messages.failed) ||
|
||||
(messages && messages.saveSuccess)
|
||||
})
|
||||
.then(async () => {
|
||||
if (action.feedback && isVisible(action.feedback, store.data)) {
|
||||
await this.openFeedback(action.feedback, store.data);
|
||||
}
|
||||
|
||||
const redirect =
|
||||
action.redirect && filter(action.redirect, store.data);
|
||||
redirect && env.jumpTo(redirect, action);
|
||||
action.reload &&
|
||||
this.reloadTarget(
|
||||
delegate || this.context,
|
||||
action.reload,
|
||||
store.data
|
||||
);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (
|
||||
action.actionType === 'copy' &&
|
||||
(action.content || action.copy)
|
||||
) {
|
||||
env.copy && env.copy(filter(action.content || action.copy, ctx, '| raw'));
|
||||
}
|
||||
}
|
||||
|
||||
handleDialogConfirm(values: object[], action: Action, ...args: Array<any>) {
|
||||
const store = this.store;
|
||||
|
||||
if (action.mergeData && values.length === 1 && values[0]) {
|
||||
store.updateData(values[0]);
|
||||
}
|
||||
|
||||
const dialog = store.action.dialog as any;
|
||||
if (
|
||||
dialog &&
|
||||
dialog.onConfirm &&
|
||||
dialog.onConfirm(values, action, ...args) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.closeDialog();
|
||||
}
|
||||
|
||||
handleDialogClose() {
|
||||
const store = this.store;
|
||||
store.closeDialog();
|
||||
}
|
||||
|
||||
handleDrawerConfirm(values: object[], action: Action, ...args: Array<any>) {
|
||||
const store = this.store;
|
||||
|
||||
if (action.mergeData && values.length === 1 && values[0]) {
|
||||
store.updateData(values[0]);
|
||||
}
|
||||
|
||||
const dialog = store.action.dialog as any;
|
||||
if (
|
||||
dialog &&
|
||||
dialog.onConfirm &&
|
||||
dialog.onConfirm(values, action, ...args) === false
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.closeDrawer();
|
||||
}
|
||||
|
||||
handleDrawerClose() {
|
||||
const store = this.store;
|
||||
store.closeDrawer();
|
||||
}
|
||||
|
||||
openFeedback(dialog: any, ctx: any) {
|
||||
return new Promise(resolve => {
|
||||
const store = this.store;
|
||||
store.setCurrentAction({
|
||||
type: 'button',
|
||||
actionType: 'dialog',
|
||||
dialog: dialog
|
||||
});
|
||||
store.openDialog(ctx, undefined, confirmed => {
|
||||
resolve(confirmed);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
reloadTarget(scoped: IScopedContext, target: string, data?: any) {
|
||||
scoped.reload(target, data);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {pathPrefix, schema, ...rest} = this.props;
|
||||
const store = this.store;
|
||||
|
||||
if (store.runtimeError) {
|
||||
return (
|
||||
<Alert level="danger">
|
||||
<h3>{this.store.runtimeError?.toString()}</h3>
|
||||
<pre>
|
||||
<code>{this.store.runtimeErrorStack.componentStack}</code>
|
||||
</pre>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
renderChild(pathPrefix!, schema, {
|
||||
...rest,
|
||||
data: this.store.downStream,
|
||||
onAction: this.handleAction
|
||||
}) as JSX.Element
|
||||
}
|
||||
|
||||
<Spinner size="lg" overlay key="info" show={store.loading} />
|
||||
|
||||
{store.error ? (
|
||||
<Alert level="danger" showCloseButton onClose={store.clearMessage}>
|
||||
{store.msg}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{renderChild(
|
||||
'dialog',
|
||||
{
|
||||
...((store.action as Action) &&
|
||||
((store.action as Action).dialog as object)),
|
||||
type: 'dialog'
|
||||
},
|
||||
{
|
||||
...rest,
|
||||
key: 'dialog',
|
||||
data: store.dialogData,
|
||||
onConfirm: this.handleDialogConfirm,
|
||||
onClose: this.handleDialogClose,
|
||||
show: store.dialogOpen,
|
||||
onAction: this.handleAction
|
||||
}
|
||||
)}
|
||||
|
||||
{renderChild(
|
||||
'drawer',
|
||||
{
|
||||
...((store.action as Action) &&
|
||||
((store.action as Action).drawer as object)),
|
||||
type: 'drawer'
|
||||
},
|
||||
{
|
||||
...rest,
|
||||
key: 'drawer',
|
||||
data: store.drawerData,
|
||||
onConfirm: this.handleDrawerConfirm,
|
||||
onClose: this.handleDrawerClose,
|
||||
show: store.drawerOpen,
|
||||
onAction: this.handleAction
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
249
src/SchemaRenderer.tsx
Normal file
249
src/SchemaRenderer.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
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 {renderChild, renderChildren} from './Root';
|
||||
import {Schema, SchemaNode} from './types';
|
||||
import {anyChanged, chainEvents} from './utils/helper';
|
||||
|
||||
interface SchemaRendererProps extends Partial<RendererProps> {
|
||||
schema: Schema;
|
||||
$path: string;
|
||||
env: RendererEnv;
|
||||
}
|
||||
|
||||
const defaultOmitList = [
|
||||
'type',
|
||||
'name',
|
||||
'$ref',
|
||||
'className',
|
||||
'data',
|
||||
'children',
|
||||
'ref',
|
||||
'visible',
|
||||
'visibleOn',
|
||||
'hidden',
|
||||
'hiddenOn',
|
||||
'disabled',
|
||||
'disabledOn',
|
||||
'component',
|
||||
'detectField',
|
||||
'required',
|
||||
'requiredOn',
|
||||
'syncSuperStore'
|
||||
];
|
||||
|
||||
export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
static displayName: string = 'Renderer';
|
||||
|
||||
renderer: RendererConfig | null;
|
||||
ref: any;
|
||||
schema: any;
|
||||
path: string;
|
||||
|
||||
constructor(props: SchemaRendererProps) {
|
||||
super(props);
|
||||
this.refFn = this.refFn.bind(this);
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.reRender = this.reRender.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resolveRenderer(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: SchemaRendererProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (
|
||||
props.schema &&
|
||||
nextProps.schema &&
|
||||
(props.schema.type !== nextProps.schema.type ||
|
||||
props.schema.$$id !== nextProps.schema.$$id)
|
||||
) {
|
||||
this.resolveRenderer(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
// 限制:只有 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, skipResolve = 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 (!skipResolve) {
|
||||
const rendererResolver = props.env.rendererResolver || resolveRenderer;
|
||||
this.renderer = rendererResolver(path, schema, props);
|
||||
}
|
||||
|
||||
return {path, schema};
|
||||
}
|
||||
|
||||
getWrappedInstance() {
|
||||
return this.ref;
|
||||
}
|
||||
|
||||
refFn(ref: any) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
renderChild(
|
||||
region: string,
|
||||
node?: SchemaNode,
|
||||
subProps: {
|
||||
data?: object;
|
||||
[propName: string]: any;
|
||||
} = {}
|
||||
) {
|
||||
let {schema, $path, env, ...rest} = this.props;
|
||||
|
||||
if (schema && schema.$ref) {
|
||||
const result = this.resolveRenderer(this.props, true);
|
||||
schema = result.schema;
|
||||
$path = result.path;
|
||||
}
|
||||
|
||||
const omitList = defaultOmitList.concat();
|
||||
if (this.renderer) {
|
||||
const Component = this.renderer.component;
|
||||
Component.propsList &&
|
||||
omitList.push.apply(omitList, Component.propsList as Array<string>);
|
||||
}
|
||||
|
||||
return renderChild(`${$path}${region ? `/${region}` : ''}`, node || '', {
|
||||
...omit(rest, omitList),
|
||||
...subProps,
|
||||
data: subProps.data || rest.data,
|
||||
env: env
|
||||
});
|
||||
}
|
||||
|
||||
reRender() {
|
||||
this.resolveRenderer(this.props);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render(): JSX.Element | null {
|
||||
let {$path, schema, ...rest} = this.props;
|
||||
|
||||
if (schema === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (schema.$ref) {
|
||||
const result = this.resolveRenderer(this.props, true);
|
||||
schema = result.schema;
|
||||
$path = result.path;
|
||||
}
|
||||
|
||||
const theme = this.props.env.theme;
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return renderChildren($path, schema as any, rest) as JSX.Element;
|
||||
} else if (schema.children) {
|
||||
return React.isValidElement(schema.children)
|
||||
? schema.children
|
||||
: (schema.children as Function)({
|
||||
...rest,
|
||||
$path: $path,
|
||||
render: this.renderChild,
|
||||
forwardedRef: this.refFn
|
||||
});
|
||||
} else if (typeof schema.component === 'function') {
|
||||
const isSFC = !(schema.component.prototype instanceof React.Component);
|
||||
return React.createElement(schema.component as any, {
|
||||
...rest,
|
||||
...schema,
|
||||
$path: $path,
|
||||
ref: isSFC ? undefined : this.refFn,
|
||||
forwardedRef: isSFC ? this.refFn : undefined,
|
||||
render: this.renderChild
|
||||
});
|
||||
} else if (Object.keys(schema).length === 0) {
|
||||
return null;
|
||||
} else if (!this.renderer) {
|
||||
return (
|
||||
<LazyComponent
|
||||
{...rest}
|
||||
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}
|
||||
retry={this.reRender}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderer = this.renderer as RendererConfig;
|
||||
schema = filterSchema(schema, renderer, rest);
|
||||
const {data: defaultData, ...restSchema} = schema;
|
||||
const Component = renderer.component;
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...theme.getRendererConfig(renderer.name)}
|
||||
{...restSchema}
|
||||
{...chainEvents(rest, restSchema)}
|
||||
defaultData={defaultData}
|
||||
$path={$path}
|
||||
ref={this.refFn}
|
||||
render={this.renderChild}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
54
src/WithRootStore.tsx
Normal file
54
src/WithRootStore.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import {IRendererStore} from './store';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
|
||||
export const RootStoreContext = React.createContext<IRendererStore>(
|
||||
undefined as any
|
||||
);
|
||||
|
||||
export function withRootStore<
|
||||
T extends React.ComponentType<
|
||||
React.ComponentProps<T> & {
|
||||
rootStore: IRendererStore;
|
||||
}
|
||||
>
|
||||
>(ComposedComponent: T) {
|
||||
type OuterProps = JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
Omit<React.ComponentProps<T>, 'rootStore'>
|
||||
>;
|
||||
|
||||
const result = hoistNonReactStatic(
|
||||
class extends React.Component<OuterProps> {
|
||||
static displayName = `WithRootStore(${
|
||||
ComposedComponent.displayName || ComposedComponent.name
|
||||
})`;
|
||||
static contextType = RootStoreContext;
|
||||
static ComposedComponent = ComposedComponent;
|
||||
|
||||
render() {
|
||||
const rootStore = this.context;
|
||||
const injectedProps: {
|
||||
rootStore: IRendererStore;
|
||||
} = {
|
||||
rootStore
|
||||
};
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...(this.props as JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
React.ComponentProps<T>
|
||||
>)}
|
||||
{...injectedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ComposedComponent
|
||||
);
|
||||
|
||||
return result as typeof result & {
|
||||
ComposedComponent: T;
|
||||
};
|
||||
}
|
276
src/WithStore.tsx
Normal file
276
src/WithStore.tsx
Normal file
@ -0,0 +1,276 @@
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import {observer} from 'mobx-react';
|
||||
import React from 'react';
|
||||
import {RendererProps} from './factory';
|
||||
import {IIRendererStore, IRendererStore} from './store';
|
||||
import {RendererData, SchemaNode} from './types';
|
||||
import getExprProperties from './utils/filter-schema';
|
||||
import {
|
||||
createObject,
|
||||
extendObject,
|
||||
guid,
|
||||
isObjectShallowModified,
|
||||
syncDataFromSuper
|
||||
} from './utils/helper';
|
||||
import {RootStoreContext} from './WithRootStore';
|
||||
|
||||
export function HocStoreFactory(renderer: {
|
||||
storeType: string;
|
||||
extendsData?: boolean;
|
||||
shouldSyncSuperStore?: (
|
||||
store: any,
|
||||
props: any,
|
||||
prevProps: any
|
||||
) => boolean | undefined;
|
||||
}): any {
|
||||
return function <T extends React.ComponentType<RendererProps>>(Component: T) {
|
||||
type Props = Omit<
|
||||
RendererProps,
|
||||
'store' | 'data' | 'dataUpdatedAt' | 'scope'
|
||||
> & {
|
||||
store?: IIRendererStore;
|
||||
data?: RendererData;
|
||||
scope?: RendererData;
|
||||
};
|
||||
|
||||
@observer
|
||||
class StoreFactory extends React.Component<Props> {
|
||||
static displayName = `WithStore(${
|
||||
Component.displayName || Component.name
|
||||
})`;
|
||||
static ComposedComponent = Component;
|
||||
static contextType = RootStoreContext;
|
||||
store: IIRendererStore;
|
||||
context!: React.ContextType<typeof RootStoreContext>;
|
||||
ref: any;
|
||||
|
||||
getWrappedInstance() {
|
||||
return this.ref;
|
||||
}
|
||||
|
||||
refFn(ref: any) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
formatData(data: any): object {
|
||||
if (Array.isArray(data)) {
|
||||
return {
|
||||
items: data
|
||||
};
|
||||
}
|
||||
|
||||
return data as object;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const rootStore = this.context;
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.refFn = this.refFn.bind(this);
|
||||
|
||||
const store = rootStore.addStore({
|
||||
id: guid(),
|
||||
path: this.props.$path,
|
||||
storeType: renderer.storeType,
|
||||
parentId: this.props.store ? this.props.store.id : ''
|
||||
}) as IIRendererStore;
|
||||
this.store = store;
|
||||
|
||||
if (renderer.extendsData === false) {
|
||||
store.initData(
|
||||
createObject(
|
||||
(this.props.data as any)
|
||||
? (this.props.data as any).__super
|
||||
: null,
|
||||
{
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
}
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
this.props.scope ||
|
||||
(this.props.data && (this.props.data as any).__super)
|
||||
) {
|
||||
if (this.props.store && this.props.data === this.props.store.data) {
|
||||
store.initData(
|
||||
createObject(this.props.store.data, {
|
||||
...this.formatData(this.props.defaultData)
|
||||
})
|
||||
);
|
||||
} else {
|
||||
store.initData(
|
||||
createObject(
|
||||
(this.props.data as any).__super || this.props.scope,
|
||||
{
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
store.initData({
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: RendererProps) {
|
||||
const props = this.props;
|
||||
const store = this.store;
|
||||
const shouldSync = renderer.shouldSyncSuperStore?.(
|
||||
store,
|
||||
nextProps,
|
||||
props
|
||||
);
|
||||
|
||||
if (shouldSync === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer.extendsData === false) {
|
||||
if (
|
||||
shouldSync === true ||
|
||||
props.defaultData !== nextProps.defaultData ||
|
||||
isObjectShallowModified(props.data, nextProps.data) ||
|
||||
//
|
||||
// 特殊处理 CRUD。
|
||||
// CRUD 中 toolbar 里面的 data 是空对象,但是 __super 会不一样
|
||||
(nextProps.data &&
|
||||
props.data &&
|
||||
nextProps.data.__super !== props.data.__super)
|
||||
) {
|
||||
store.initData(
|
||||
extendObject(nextProps.data, {
|
||||
...(store.hasRemoteData ? store.data : null), // todo 只保留 remote 数据
|
||||
...this.formatData(nextProps.defaultData),
|
||||
...this.formatData(nextProps.data)
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
shouldSync === true ||
|
||||
isObjectShallowModified(props.data, nextProps.data)
|
||||
) {
|
||||
if (nextProps.store && nextProps.store.data === nextProps.data) {
|
||||
store.initData(
|
||||
createObject(
|
||||
nextProps.store.data,
|
||||
nextProps.syncSuperStore === false
|
||||
? {
|
||||
...store.data
|
||||
}
|
||||
: syncDataFromSuper(
|
||||
store.data,
|
||||
nextProps.store.data,
|
||||
props.scope,
|
||||
store,
|
||||
nextProps.syncSuperStore === true
|
||||
)
|
||||
)
|
||||
);
|
||||
} else if (nextProps.data && (nextProps.data as any).__super) {
|
||||
store.initData(extendObject(nextProps.data));
|
||||
} else {
|
||||
store.initData(createObject(nextProps.scope, nextProps.data));
|
||||
}
|
||||
} else if (
|
||||
(shouldSync === true ||
|
||||
!nextProps.store ||
|
||||
nextProps.data !== nextProps.store.data) &&
|
||||
nextProps.data &&
|
||||
nextProps.data.__super
|
||||
) {
|
||||
// 这个用法很少,当 data.__super 值发生变化时,更新 store.data
|
||||
(!props.data ||
|
||||
isObjectShallowModified(
|
||||
nextProps.data.__super,
|
||||
props.data.__super,
|
||||
false
|
||||
)) &&
|
||||
// nextProps.data.__super !== props.data.__super) &&
|
||||
store.initData(
|
||||
createObject(nextProps.data.__super, {
|
||||
...nextProps.data,
|
||||
...store.data
|
||||
}),
|
||||
|
||||
store.storeType === 'FormStore' &&
|
||||
props.store?.storeType === 'CRUDStore'
|
||||
);
|
||||
} else if (
|
||||
nextProps.scope &&
|
||||
nextProps.data === nextProps.store!.data &&
|
||||
(shouldSync === true || props.data !== nextProps.data)
|
||||
) {
|
||||
store.initData(
|
||||
createObject(nextProps.scope, {
|
||||
// ...nextProps.data,
|
||||
...store.data
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const rootStore = this.context as IRendererStore;
|
||||
const store = this.store;
|
||||
rootStore.removeStore(store);
|
||||
|
||||
// @ts-ignore
|
||||
delete this.store;
|
||||
}
|
||||
|
||||
renderChild(
|
||||
region: string,
|
||||
node: SchemaNode,
|
||||
subProps: {
|
||||
data?: object;
|
||||
[propName: string]: any;
|
||||
} = {}
|
||||
) {
|
||||
let {render} = this.props;
|
||||
|
||||
return render(region, node, {
|
||||
data: this.store.data,
|
||||
dataUpdatedAt: this.store.updatedAt,
|
||||
...subProps,
|
||||
scope: this.store.data,
|
||||
store: this.store
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {detectField, ...rest} = this.props;
|
||||
|
||||
let exprProps: any = {};
|
||||
if (!detectField || detectField === 'data') {
|
||||
exprProps = getExprProperties(rest, this.store.data, undefined, rest);
|
||||
|
||||
if (exprProps.hidden || exprProps.visible === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
{
|
||||
...(rest as any) /* todo */
|
||||
}
|
||||
{...exprProps}
|
||||
ref={this.refFn}
|
||||
data={this.store.data}
|
||||
dataUpdatedAt={this.store.updatedAt}
|
||||
store={this.store}
|
||||
scope={this.store.data}
|
||||
render={this.renderChild}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
hoistNonReactStatic(StoreFactory, Component);
|
||||
|
||||
return StoreFactory;
|
||||
};
|
||||
}
|
809
src/factory.tsx
809
src/factory.tsx
@ -2,61 +2,20 @@ import React from 'react';
|
||||
import qs from 'qs';
|
||||
import {RendererStore, IRendererStore, IIRendererStore} from './store/index';
|
||||
import {getEnv, destroy} from 'mobx-state-tree';
|
||||
import {Location} from 'history';
|
||||
import {wrapFetcher} from './utils/api';
|
||||
import {normalizeLink} from './utils/normalizeLink';
|
||||
import {
|
||||
createObject,
|
||||
extendObject,
|
||||
guid,
|
||||
findIndex,
|
||||
promisify,
|
||||
anyChanged,
|
||||
syncDataFromSuper,
|
||||
isObjectShallowModified,
|
||||
isEmpty,
|
||||
autobind,
|
||||
chainEvents
|
||||
} from './utils/helper';
|
||||
import {
|
||||
Api,
|
||||
fetcherResult,
|
||||
Payload,
|
||||
SchemaNode,
|
||||
Schema,
|
||||
Action,
|
||||
RendererData
|
||||
} from './types';
|
||||
import {findIndex, promisify} from './utils/helper';
|
||||
import {Api, fetcherResult, Payload, SchemaNode, Schema, Action} from './types';
|
||||
import {observer} from 'mobx-react';
|
||||
import getExprProperties from './utils/filter-schema';
|
||||
import hoistNonReactStatic from 'hoist-non-react-statics';
|
||||
import omit from 'lodash/omit';
|
||||
import difference from 'lodash/difference';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import Scoped from './Scoped';
|
||||
import {
|
||||
getTheme,
|
||||
ThemeInstance,
|
||||
ClassNamesFn,
|
||||
ThemeContext,
|
||||
ThemeProps
|
||||
} from './theme';
|
||||
import {getTheme, ThemeInstance, ThemeProps} from './theme';
|
||||
import find from 'lodash/find';
|
||||
import Alert from './components/Alert2';
|
||||
import {toast} from './components/Toast';
|
||||
import {alert, confirm, setRenderSchemaFn} from './components/Alert';
|
||||
import {LazyComponent} from './components';
|
||||
import ImageGallery from './components/ImageGallery';
|
||||
import {
|
||||
TranslateFn,
|
||||
getDefaultLocale,
|
||||
makeTranslator,
|
||||
LocaleContext,
|
||||
LocaleProps
|
||||
} from './locale';
|
||||
import {SchemaCollection, SchemaObject, SchemaTpl} from './Schema';
|
||||
import {result} from 'lodash';
|
||||
import {envOverwrite} from './envOverwrite';
|
||||
import {getDefaultLocale, makeTranslator, LocaleProps} from './locale';
|
||||
import ScopedRootRenderer, {RootRenderProps} from './Root';
|
||||
import {HocStoreFactory} from './WithStore';
|
||||
|
||||
export interface TestFunc {
|
||||
(
|
||||
@ -148,10 +107,6 @@ export interface RendererProps extends ThemeProps, LocaleProps {
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface renderChildProps extends Partial<RendererProps> {
|
||||
env: RendererEnv;
|
||||
}
|
||||
|
||||
export type RendererComponent = React.ComponentType<RendererProps> & {
|
||||
propsList?: Array<any>;
|
||||
};
|
||||
@ -165,12 +120,6 @@ export interface RenderSchemaFilter {
|
||||
(schema: Schema, renderer: RendererConfig, props?: any): Schema;
|
||||
}
|
||||
|
||||
export interface RootRenderProps {
|
||||
location?: Location;
|
||||
theme?: string;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface RenderOptions {
|
||||
session?: string;
|
||||
fetcher?: (config: fetcherConfig) => Promise<fetcherResult>;
|
||||
@ -204,8 +153,6 @@ export interface fetcherConfig {
|
||||
config?: any;
|
||||
}
|
||||
|
||||
export type ReactElement = React.ReactNode[] | JSX.Element | null | false;
|
||||
|
||||
const renderers: Array<RendererConfig> = [];
|
||||
const rendererNames: Array<string> = [];
|
||||
const schemaFilters: Array<RenderSchemaFilter> = [];
|
||||
@ -291,682 +238,7 @@ export function unRegisterRenderer(config: RendererConfig | string) {
|
||||
cache = {};
|
||||
}
|
||||
|
||||
export function renderChildren(
|
||||
prefix: string,
|
||||
node: SchemaNode,
|
||||
props: renderChildProps
|
||||
): ReactElement {
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((node, index) =>
|
||||
renderChild(`${prefix}/${index}`, node, {
|
||||
...props,
|
||||
key: `${props.key ? `${props.key}-` : ''}${index}`
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return renderChild(prefix, node, props);
|
||||
}
|
||||
|
||||
export function renderChild(
|
||||
prefix: string,
|
||||
node: SchemaNode,
|
||||
props: renderChildProps
|
||||
): ReactElement {
|
||||
if (Array.isArray(node)) {
|
||||
return renderChildren(prefix, node, props);
|
||||
}
|
||||
|
||||
const typeofnode = typeof node;
|
||||
let schema: Schema =
|
||||
typeofnode === 'string' || typeofnode === 'number'
|
||||
? {type: 'tpl', tpl: String(node)}
|
||||
: (node as Schema);
|
||||
const detectData =
|
||||
schema &&
|
||||
(schema.detectField === '&' ? props : props[schema.detectField || 'data']);
|
||||
const exprProps = detectData
|
||||
? getExprProperties(schema, detectData, undefined, props)
|
||||
: null;
|
||||
|
||||
if (
|
||||
exprProps &&
|
||||
(exprProps.hidden ||
|
||||
exprProps.visible === false ||
|
||||
schema.hidden ||
|
||||
schema.visible === false ||
|
||||
props.hidden ||
|
||||
props.visible === false)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const transform = props.propsTransform;
|
||||
|
||||
if (transform) {
|
||||
// @ts-ignore
|
||||
delete props.propsTransform;
|
||||
props = transform(props);
|
||||
}
|
||||
|
||||
return (
|
||||
<SchemaRenderer
|
||||
{...props}
|
||||
{...exprProps}
|
||||
schema={schema}
|
||||
$path={`${prefix ? `${prefix}/` : ''}${(schema && schema.type) || ''}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface RootRendererProps {
|
||||
schema: SchemaNode;
|
||||
rootStore: IRendererStore;
|
||||
env: RendererEnv;
|
||||
theme: string;
|
||||
pathPrefix?: string;
|
||||
locale?: string;
|
||||
translate?: TranslateFn;
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export const RootStoreContext = React.createContext<IRendererStore>(
|
||||
undefined as any
|
||||
);
|
||||
|
||||
export class RootRenderer extends React.Component<RootRendererProps> {
|
||||
state = {
|
||||
error: null,
|
||||
errorInfo: null
|
||||
};
|
||||
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
console.error(error);
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
resolveDefinitions(name: string) {
|
||||
const definitions = (this.props.schema as Schema).definitions;
|
||||
if (!name || isEmpty(definitions)) {
|
||||
return {};
|
||||
}
|
||||
return definitions && definitions[name];
|
||||
}
|
||||
|
||||
render() {
|
||||
const {error, errorInfo} = this.state;
|
||||
if (errorInfo) {
|
||||
return errorRenderer(error, errorInfo);
|
||||
}
|
||||
const {
|
||||
schema,
|
||||
rootStore,
|
||||
env,
|
||||
pathPrefix,
|
||||
location,
|
||||
data,
|
||||
locale,
|
||||
translate,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
const theme = env.theme;
|
||||
const query =
|
||||
(location && location.query) ||
|
||||
(location && location.search && qs.parse(location.search.substring(1))) ||
|
||||
(window.location.search && qs.parse(window.location.search.substring(1)));
|
||||
|
||||
const finalData = query
|
||||
? createObject(
|
||||
{
|
||||
...(data && data.__super ? data.__super : null),
|
||||
...query,
|
||||
__query: query
|
||||
},
|
||||
data
|
||||
)
|
||||
: data;
|
||||
|
||||
// 根据环境覆盖 schema,这个要在最前面做,不然就无法覆盖 validations
|
||||
envOverwrite(schema, locale);
|
||||
|
||||
return (
|
||||
<RootStoreContext.Provider value={rootStore}>
|
||||
<ThemeContext.Provider value={this.props.theme || 'default'}>
|
||||
<LocaleContext.Provider value={this.props.locale!}>
|
||||
<ImageGallery modalContainer={env.getModalContainer}>
|
||||
{
|
||||
renderChild(
|
||||
pathPrefix || '',
|
||||
isPlainObject(schema)
|
||||
? {
|
||||
type: 'page',
|
||||
...(schema as any)
|
||||
}
|
||||
: schema,
|
||||
{
|
||||
...rest,
|
||||
resolveDefinitions: this.resolveDefinitions,
|
||||
location: location,
|
||||
data: finalData,
|
||||
env,
|
||||
classnames: theme.classnames,
|
||||
classPrefix: theme.classPrefix,
|
||||
locale,
|
||||
translate
|
||||
}
|
||||
) as JSX.Element
|
||||
}
|
||||
</ImageGallery>
|
||||
</LocaleContext.Provider>
|
||||
</ThemeContext.Provider>
|
||||
</RootStoreContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const ScopedRootRenderer = Scoped(RootRenderer);
|
||||
|
||||
interface SchemaRendererProps extends Partial<RendererProps> {
|
||||
schema: Schema;
|
||||
$path: string;
|
||||
env: RendererEnv;
|
||||
}
|
||||
|
||||
const defaultOmitList = [
|
||||
'type',
|
||||
'name',
|
||||
'$ref',
|
||||
'className',
|
||||
'data',
|
||||
'children',
|
||||
'ref',
|
||||
'visible',
|
||||
'visibleOn',
|
||||
'hidden',
|
||||
'hiddenOn',
|
||||
'disabled',
|
||||
'disabledOn',
|
||||
'component',
|
||||
'detectField',
|
||||
'required',
|
||||
'requiredOn',
|
||||
'syncSuperStore'
|
||||
];
|
||||
|
||||
class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
|
||||
static displayName: string = 'Renderer';
|
||||
|
||||
renderer: RendererConfig | null;
|
||||
ref: any;
|
||||
schema: any;
|
||||
path: string;
|
||||
|
||||
constructor(props: SchemaRendererProps) {
|
||||
super(props);
|
||||
this.refFn = this.refFn.bind(this);
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.reRender = this.reRender.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resolveRenderer(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: SchemaRendererProps) {
|
||||
const props = this.props;
|
||||
|
||||
if (
|
||||
props.schema &&
|
||||
nextProps.schema &&
|
||||
(props.schema.type !== nextProps.schema.type ||
|
||||
props.schema.$$id !== nextProps.schema.$$id)
|
||||
) {
|
||||
this.resolveRenderer(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
// 限制:只有 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, skipResolve = 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 (!skipResolve) {
|
||||
const rendererResolver = props.env.rendererResolver || resolveRenderer;
|
||||
this.renderer = rendererResolver(path, schema, props);
|
||||
}
|
||||
|
||||
return {path, schema};
|
||||
}
|
||||
|
||||
getWrappedInstance() {
|
||||
return this.ref;
|
||||
}
|
||||
|
||||
refFn(ref: any) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
renderChild(
|
||||
region: string,
|
||||
node?: SchemaNode,
|
||||
subProps: {
|
||||
data?: object;
|
||||
[propName: string]: any;
|
||||
} = {}
|
||||
) {
|
||||
let {schema, $path, env, ...rest} = this.props;
|
||||
|
||||
if (schema && schema.$ref) {
|
||||
const result = this.resolveRenderer(this.props, true);
|
||||
schema = result.schema;
|
||||
$path = result.path;
|
||||
}
|
||||
|
||||
const omitList = defaultOmitList.concat();
|
||||
if (this.renderer) {
|
||||
const Component = this.renderer.component;
|
||||
Component.propsList &&
|
||||
omitList.push.apply(omitList, Component.propsList as Array<string>);
|
||||
}
|
||||
|
||||
return renderChild(`${$path}${region ? `/${region}` : ''}`, node || '', {
|
||||
...omit(rest, omitList),
|
||||
...subProps,
|
||||
data: subProps.data || rest.data,
|
||||
env: env
|
||||
});
|
||||
}
|
||||
|
||||
reRender() {
|
||||
this.resolveRenderer(this.props);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render(): JSX.Element | null {
|
||||
let {$path, schema, ...rest} = this.props;
|
||||
|
||||
if (schema === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (schema.$ref) {
|
||||
const result = this.resolveRenderer(this.props, true);
|
||||
schema = result.schema;
|
||||
$path = result.path;
|
||||
}
|
||||
|
||||
const theme = this.props.env.theme;
|
||||
|
||||
if (Array.isArray(schema)) {
|
||||
return renderChildren($path, schema as any, rest) as JSX.Element;
|
||||
} else if (schema.children) {
|
||||
return React.isValidElement(schema.children)
|
||||
? schema.children
|
||||
: (schema.children as Function)({
|
||||
...rest,
|
||||
$path: $path,
|
||||
render: this.renderChild,
|
||||
forwardedRef: this.refFn
|
||||
});
|
||||
} else if (typeof schema.component === 'function') {
|
||||
const isSFC = !(schema.component.prototype instanceof React.Component);
|
||||
return React.createElement(schema.component as any, {
|
||||
...rest,
|
||||
...schema,
|
||||
$path: $path,
|
||||
ref: isSFC ? undefined : this.refFn,
|
||||
forwardedRef: isSFC ? this.refFn : undefined,
|
||||
render: this.renderChild
|
||||
});
|
||||
} else if (Object.keys(schema).length === 0) {
|
||||
return null;
|
||||
} else if (!this.renderer) {
|
||||
return (
|
||||
<LazyComponent
|
||||
{...rest}
|
||||
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}
|
||||
retry={this.reRender}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const renderer = this.renderer as RendererConfig;
|
||||
schema = filterSchema(schema, renderer, rest);
|
||||
const {data: defaultData, ...restSchema} = schema;
|
||||
const Component = renderer.component;
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...theme.getRendererConfig(renderer.name)}
|
||||
{...restSchema}
|
||||
{...chainEvents(rest, restSchema)}
|
||||
defaultData={defaultData}
|
||||
$path={$path}
|
||||
ref={this.refFn}
|
||||
render={this.renderChild}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function HocStoreFactory(renderer: {
|
||||
storeType: string;
|
||||
extendsData?: boolean;
|
||||
shouldSyncSuperStore?: (
|
||||
store: any,
|
||||
props: any,
|
||||
prevProps: any
|
||||
) => boolean | undefined;
|
||||
}): any {
|
||||
return function <T extends React.ComponentType<RendererProps>>(Component: T) {
|
||||
type Props = Omit<
|
||||
RendererProps,
|
||||
'store' | 'data' | 'dataUpdatedAt' | 'scope'
|
||||
> & {
|
||||
store?: IIRendererStore;
|
||||
data?: RendererData;
|
||||
scope?: RendererData;
|
||||
};
|
||||
|
||||
@observer
|
||||
class StoreFactory extends React.Component<Props> {
|
||||
static displayName = `WithStore(${
|
||||
Component.displayName || Component.name
|
||||
})`;
|
||||
static ComposedComponent = Component;
|
||||
static contextType = RootStoreContext;
|
||||
store: IIRendererStore;
|
||||
context!: React.ContextType<typeof RootStoreContext>;
|
||||
ref: any;
|
||||
|
||||
getWrappedInstance() {
|
||||
return this.ref;
|
||||
}
|
||||
|
||||
refFn(ref: any) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
formatData(data: any): object {
|
||||
if (Array.isArray(data)) {
|
||||
return {
|
||||
items: data
|
||||
};
|
||||
}
|
||||
|
||||
return data as object;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const rootStore = this.context;
|
||||
this.renderChild = this.renderChild.bind(this);
|
||||
this.refFn = this.refFn.bind(this);
|
||||
|
||||
const store = rootStore.addStore({
|
||||
id: guid(),
|
||||
path: this.props.$path,
|
||||
storeType: renderer.storeType,
|
||||
parentId: this.props.store ? this.props.store.id : ''
|
||||
}) as IIRendererStore;
|
||||
this.store = store;
|
||||
|
||||
if (renderer.extendsData === false) {
|
||||
store.initData(
|
||||
createObject(
|
||||
(this.props.data as any)
|
||||
? (this.props.data as any).__super
|
||||
: null,
|
||||
{
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
}
|
||||
)
|
||||
);
|
||||
} else if (
|
||||
this.props.scope ||
|
||||
(this.props.data && (this.props.data as any).__super)
|
||||
) {
|
||||
if (this.props.store && this.props.data === this.props.store.data) {
|
||||
store.initData(
|
||||
createObject(this.props.store.data, {
|
||||
...this.formatData(this.props.defaultData)
|
||||
})
|
||||
);
|
||||
} else {
|
||||
store.initData(
|
||||
createObject(
|
||||
(this.props.data as any).__super || this.props.scope,
|
||||
{
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
store.initData({
|
||||
...this.formatData(this.props.defaultData),
|
||||
...this.formatData(this.props.data)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: RendererProps) {
|
||||
const props = this.props;
|
||||
const store = this.store;
|
||||
const shouldSync = renderer.shouldSyncSuperStore?.(
|
||||
store,
|
||||
nextProps,
|
||||
props
|
||||
);
|
||||
|
||||
if (shouldSync === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (renderer.extendsData === false) {
|
||||
if (
|
||||
shouldSync === true ||
|
||||
props.defaultData !== nextProps.defaultData ||
|
||||
isObjectShallowModified(props.data, nextProps.data) ||
|
||||
//
|
||||
// 特殊处理 CRUD。
|
||||
// CRUD 中 toolbar 里面的 data 是空对象,但是 __super 会不一样
|
||||
(nextProps.data &&
|
||||
props.data &&
|
||||
nextProps.data.__super !== props.data.__super)
|
||||
) {
|
||||
store.initData(
|
||||
extendObject(nextProps.data, {
|
||||
...(store.hasRemoteData ? store.data : null), // todo 只保留 remote 数据
|
||||
...this.formatData(nextProps.defaultData),
|
||||
...this.formatData(nextProps.data)
|
||||
})
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
shouldSync === true ||
|
||||
isObjectShallowModified(props.data, nextProps.data)
|
||||
) {
|
||||
if (nextProps.store && nextProps.store.data === nextProps.data) {
|
||||
store.initData(
|
||||
createObject(
|
||||
nextProps.store.data,
|
||||
nextProps.syncSuperStore === false
|
||||
? {
|
||||
...store.data
|
||||
}
|
||||
: syncDataFromSuper(
|
||||
store.data,
|
||||
nextProps.store.data,
|
||||
props.scope,
|
||||
store,
|
||||
nextProps.syncSuperStore === true
|
||||
)
|
||||
)
|
||||
);
|
||||
} else if (nextProps.data && (nextProps.data as any).__super) {
|
||||
store.initData(extendObject(nextProps.data));
|
||||
} else {
|
||||
store.initData(createObject(nextProps.scope, nextProps.data));
|
||||
}
|
||||
} else if (
|
||||
(shouldSync === true ||
|
||||
!nextProps.store ||
|
||||
nextProps.data !== nextProps.store.data) &&
|
||||
nextProps.data &&
|
||||
nextProps.data.__super
|
||||
) {
|
||||
// 这个用法很少,当 data.__super 值发生变化时,更新 store.data
|
||||
(!props.data ||
|
||||
isObjectShallowModified(
|
||||
nextProps.data.__super,
|
||||
props.data.__super,
|
||||
false
|
||||
)) &&
|
||||
// nextProps.data.__super !== props.data.__super) &&
|
||||
store.initData(
|
||||
createObject(nextProps.data.__super, {
|
||||
...nextProps.data,
|
||||
...store.data
|
||||
}),
|
||||
|
||||
store.storeType === 'FormStore' &&
|
||||
props.store?.storeType === 'CRUDStore'
|
||||
);
|
||||
} else if (
|
||||
nextProps.scope &&
|
||||
nextProps.data === nextProps.store!.data &&
|
||||
(shouldSync === true || props.data !== nextProps.data)
|
||||
) {
|
||||
store.initData(
|
||||
createObject(nextProps.scope, {
|
||||
// ...nextProps.data,
|
||||
...store.data
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const rootStore = this.context as IRendererStore;
|
||||
const store = this.store;
|
||||
rootStore.removeStore(store);
|
||||
|
||||
// @ts-ignore
|
||||
delete this.store;
|
||||
}
|
||||
|
||||
renderChild(
|
||||
region: string,
|
||||
node: SchemaNode,
|
||||
subProps: {
|
||||
data?: object;
|
||||
[propName: string]: any;
|
||||
} = {}
|
||||
) {
|
||||
let {render} = this.props;
|
||||
|
||||
return render(region, node, {
|
||||
data: this.store.data,
|
||||
dataUpdatedAt: this.store.updatedAt,
|
||||
...subProps,
|
||||
scope: this.store.data,
|
||||
store: this.store
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {detectField, ...rest} = this.props;
|
||||
|
||||
let exprProps: any = {};
|
||||
if (!detectField || detectField === 'data') {
|
||||
exprProps = getExprProperties(rest, this.store.data, undefined, rest);
|
||||
|
||||
if (exprProps.hidden || exprProps.visible === false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Component
|
||||
{
|
||||
...(rest as any) /* todo */
|
||||
}
|
||||
{...exprProps}
|
||||
ref={this.refFn}
|
||||
data={this.store.data}
|
||||
dataUpdatedAt={this.store.updatedAt}
|
||||
store={this.store}
|
||||
scope={this.store.data}
|
||||
render={this.renderChild}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
hoistNonReactStatic(StoreFactory, Component);
|
||||
|
||||
return StoreFactory;
|
||||
};
|
||||
}
|
||||
|
||||
function loadRenderer(schema: Schema, path: string) {
|
||||
export function loadRenderer(schema: Schema, path: string) {
|
||||
return (
|
||||
<Alert level="danger">
|
||||
<p>Error: 找不到对应的渲染器</p>
|
||||
@ -978,17 +250,6 @@ function loadRenderer(schema: Schema, path: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function errorRenderer(error: any, errorInfo: any) {
|
||||
return (
|
||||
<Alert level="danger">
|
||||
<p>{error && error.toString()}</p>
|
||||
<pre>
|
||||
<code>{errorInfo.componentStack}</code>
|
||||
</pre>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultOptions: RenderOptions = {
|
||||
session: 'global',
|
||||
affixOffsetTop: 50,
|
||||
@ -1143,7 +404,7 @@ export function clearStoresCache(
|
||||
}
|
||||
|
||||
// 当然也可以直接这样更新。
|
||||
// 主要是有时候第一创建的时候并没有准备多少接口,
|
||||
// 主要是有时候第一次创建的时候并没有准备多少接口,
|
||||
// 可以后续补充点,比如 amis 自己实现的,prompt 里面的表单。
|
||||
export function updateEnv(options: Partial<RenderOptions>, session = 'global') {
|
||||
options = {
|
||||
@ -1171,8 +432,7 @@ export function updateEnv(options: Partial<RenderOptions>, session = 'global') {
|
||||
let cache: {[propName: string]: RendererConfig} = {};
|
||||
export function resolveRenderer(
|
||||
path: string,
|
||||
schema?: Schema,
|
||||
props?: any
|
||||
schema?: Schema
|
||||
): null | RendererConfig {
|
||||
if (cache[path]) {
|
||||
return cache[path];
|
||||
@ -1185,6 +445,7 @@ export function resolveRenderer(
|
||||
renderers.some(item => {
|
||||
let matched = false;
|
||||
|
||||
// 不应该搞得这么复杂的,让每个渲染器唯一 id,自己不晕别人用起来也不晕。
|
||||
if (typeof item.test === 'function') {
|
||||
matched = item.test(path, schema, resolveRenderer);
|
||||
} else if (item.test instanceof RegExp) {
|
||||
@ -1198,7 +459,8 @@ export function resolveRenderer(
|
||||
return matched;
|
||||
});
|
||||
|
||||
// 只能缓存纯正则表达式的后者方法中没有用到第二个参数的,因为自定义 test 函数的有可能依赖 schema 的结果
|
||||
// 只能缓存纯正则表达式的后者方法中没有用到第二个参数的,
|
||||
// 因为自定义 test 函数的有可能依赖 schema 的结果
|
||||
if (
|
||||
renderer !== null &&
|
||||
((renderer as RendererConfig).test instanceof RegExp ||
|
||||
@ -1219,53 +481,6 @@ export function getRendererByName(name: string) {
|
||||
return find(renderers, item => item.name === name);
|
||||
}
|
||||
|
||||
export function withRootStore<
|
||||
T extends React.ComponentType<
|
||||
React.ComponentProps<T> & {
|
||||
rootStore: IRendererStore;
|
||||
}
|
||||
>
|
||||
>(ComposedComponent: T) {
|
||||
type OuterProps = JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
Omit<React.ComponentProps<T>, 'rootStore'>
|
||||
>;
|
||||
|
||||
const result = hoistNonReactStatic(
|
||||
class extends React.Component<OuterProps> {
|
||||
static displayName = `WithRootStore(${
|
||||
ComposedComponent.displayName || ComposedComponent.name
|
||||
})`;
|
||||
static contextType = RootStoreContext;
|
||||
static ComposedComponent = ComposedComponent;
|
||||
|
||||
render() {
|
||||
const rootStore = this.context;
|
||||
const injectedProps: {
|
||||
rootStore: IRendererStore;
|
||||
} = {
|
||||
rootStore
|
||||
};
|
||||
|
||||
return (
|
||||
<ComposedComponent
|
||||
{...(this.props as JSX.LibraryManagedAttributes<
|
||||
T,
|
||||
React.ComponentProps<T>
|
||||
>)}
|
||||
{...injectedProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
ComposedComponent
|
||||
);
|
||||
|
||||
return result as typeof result & {
|
||||
ComposedComponent: T;
|
||||
};
|
||||
}
|
||||
|
||||
setRenderSchemaFn((controls, value, callback, scopeRef, theme) => {
|
||||
return render(
|
||||
{
|
||||
|
@ -2,12 +2,7 @@ import React from 'react';
|
||||
import {IFormStore, IFormItemStore} from '../../store/form';
|
||||
import debouce from 'lodash/debounce';
|
||||
|
||||
import {
|
||||
RendererProps,
|
||||
Renderer,
|
||||
RootStoreContext,
|
||||
withRootStore
|
||||
} from '../../factory';
|
||||
import {RendererProps, Renderer} from '../../factory';
|
||||
import {ComboStore, IComboStore, IUniqueGroup} from '../../store/combo';
|
||||
import {
|
||||
anyChanged,
|
||||
@ -23,6 +18,7 @@ import {reaction} from 'mobx';
|
||||
import {FormItemStore} from '../../store/formItem';
|
||||
import {isAlive} from 'mobx-state-tree';
|
||||
import {observer} from 'mobx-react';
|
||||
import {withRootStore} from '../../WithRootStore';
|
||||
|
||||
export interface ControlProps extends RendererProps {
|
||||
control: {
|
||||
|
@ -7,8 +7,7 @@ import {
|
||||
RendererProps,
|
||||
registerRenderer,
|
||||
TestFunc,
|
||||
RendererConfig,
|
||||
HocStoreFactory
|
||||
RendererConfig
|
||||
} from '../../factory';
|
||||
import {anyChanged, ucFirst, getWidthRate, autobind} from '../../utils/helper';
|
||||
import {observer} from 'mobx-react';
|
||||
@ -79,6 +78,7 @@ import {UUIDControlSchema} from './UUID';
|
||||
import {PlainSchema} from '../Plain';
|
||||
import {TplSchema} from '../Tpl';
|
||||
import {DividerSchema} from '../Divider';
|
||||
import {HocStoreFactory} from '../../WithStore';
|
||||
|
||||
export type FormControlType =
|
||||
| 'array'
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
SchemaMessage
|
||||
} from '../Schema';
|
||||
import {SchemaRemark} from './Remark';
|
||||
import {onAction} from 'mobx-state-tree';
|
||||
|
||||
/**
|
||||
* amis Page 渲染器。详情请见:https://baidu.gitee.io/amis/docs/components/page
|
||||
@ -255,27 +256,9 @@ export default class Page extends React.Component<PageProps> {
|
||||
throwErrors: boolean = false,
|
||||
delegate?: IScopedContext
|
||||
) {
|
||||
const {env, store, messages} = this.props;
|
||||
const {env, store, messages, onAction} = this.props;
|
||||
|
||||
if (
|
||||
action.actionType === 'url' ||
|
||||
action.actionType === 'link' ||
|
||||
action.actionType === 'jump'
|
||||
) {
|
||||
if (!env || !env.jumpTo) {
|
||||
throw new Error('env.jumpTo is required!');
|
||||
}
|
||||
|
||||
env.jumpTo(
|
||||
filter(
|
||||
(action.to || action.url || action.link) as string,
|
||||
ctx,
|
||||
'| raw'
|
||||
),
|
||||
action,
|
||||
ctx
|
||||
);
|
||||
} else if (action.actionType === 'dialog') {
|
||||
if (action.actionType === 'dialog') {
|
||||
store.setCurrentAction(action);
|
||||
store.openDialog(ctx);
|
||||
} else if (action.actionType === 'drawer') {
|
||||
@ -303,11 +286,8 @@ export default class Page extends React.Component<PageProps> {
|
||||
action.reload && this.reloadTarget(action.reload, store.data);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else if (
|
||||
action.actionType === 'copy' &&
|
||||
(action.content || action.copy)
|
||||
) {
|
||||
env.copy && env.copy(filter(action.content || action.copy, ctx, '| raw'));
|
||||
} else {
|
||||
onAction(e, action, ctx, throwErrors, delegate || this.context);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -497,12 +497,11 @@ export const FormItemStore = StoreNode.named('FormItemStore')
|
||||
} catch (e) {
|
||||
const env = getEnv(self);
|
||||
|
||||
self.loading = false;
|
||||
|
||||
if (!isAlive(self) || self.disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.loading = false;
|
||||
if (env.isCancel(e)) {
|
||||
return;
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import {FormItemStore} from './formItem';
|
||||
import {addStore, getStoreById, getStores, removeStore} from './manager';
|
||||
import {PaginationStore} from './pagination';
|
||||
import {AppStore} from './app';
|
||||
import {RootStore} from './root';
|
||||
|
||||
setLivelynessChecking(
|
||||
process.env.NODE_ENV === 'production' ? 'ignore' : 'error'
|
||||
@ -76,6 +77,10 @@ export const RendererStore = types
|
||||
parentId?: string;
|
||||
[propName: string]: any;
|
||||
}): IStoreNode {
|
||||
if (store.storeType === RootStore.name) {
|
||||
return addStore(RootStore.create(store, getEnv(self)));
|
||||
}
|
||||
|
||||
const factory = find(
|
||||
allowedStoreList,
|
||||
item => item.name === store.storeType
|
||||
|
44
src/store/root.ts
Normal file
44
src/store/root.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {Instance, types} from 'mobx-state-tree';
|
||||
import qs from 'qs';
|
||||
import {createObject} from '../utils/helper';
|
||||
import {ServiceStore} from './service';
|
||||
|
||||
export const RootStore = ServiceStore.named('RootStore')
|
||||
.props({
|
||||
runtimeError: types.frozen(),
|
||||
runtimeErrorStack: types.frozen(),
|
||||
query: types.frozen()
|
||||
})
|
||||
.views(self => ({
|
||||
get downStream() {
|
||||
return self.query
|
||||
? createObject(
|
||||
{
|
||||
...(self.data && self.data.__super ? self.data.__super : null),
|
||||
...self.query,
|
||||
__query: self.query
|
||||
},
|
||||
self.data
|
||||
)
|
||||
: self.data;
|
||||
}
|
||||
}))
|
||||
.actions(self => ({
|
||||
setRuntimeError(error: any, errorStack: any) {
|
||||
self.runtimeError = error;
|
||||
self.runtimeErrorStack = errorStack;
|
||||
},
|
||||
updateLocation(location?: any) {
|
||||
const query =
|
||||
(location && location.query) ||
|
||||
(location &&
|
||||
location.search &&
|
||||
qs.parse(location.search.substring(1))) ||
|
||||
(window.location.search &&
|
||||
qs.parse(window.location.search.substring(1)));
|
||||
|
||||
self.query = query;
|
||||
}
|
||||
}));
|
||||
|
||||
export type IRootStore = Instance<typeof RootStore>;
|
Loading…
Reference in New Issue
Block a user