feat: 渲染器包裹支持外层自定义 (#11138)

This commit is contained in:
liaoxuezhi 2024-10-30 14:37:36 +08:00 committed by 2betop
parent 38b667f03f
commit 2cb217aa36
8 changed files with 94 additions and 103 deletions

View File

@ -11,7 +11,11 @@ import {ThemeContext} from './theme';
import {Schema, SchemaNode} from './types'; import {Schema, SchemaNode} from './types';
import {autobind, isEmpty} from './utils/helper'; import {autobind, isEmpty} from './utils/helper';
import {RootStoreContext} from './WithRootStore'; import {RootStoreContext} from './WithRootStore';
import {StatusScoped, StatusScopedProps} from './StatusScoped'; import {
StatusScoped,
StatusScopedWrapper,
StatusScopedProps
} from './StatusScoped';
export interface RootRenderProps { export interface RootRenderProps {
location?: Location; location?: Location;
@ -154,8 +158,6 @@ export interface renderChildProps
} }
export type ReactElement = React.ReactNode[] | JSX.Element | null | false; export type ReactElement = React.ReactNode[] | JSX.Element | null | false;
const StatusScopedSchemaRenderer = StatusScoped(SchemaRenderer);
export function renderChildren( export function renderChildren(
prefix: string, prefix: string,
node: SchemaNode, node: SchemaNode,
@ -206,6 +208,8 @@ export function renderChild(
props = transform(props); props = transform(props);
} }
const Comp = props.env.SchemaRenderer || SchemaRenderer;
if ( if (
['dialog', 'drawer'].includes(schema?.type) && ['dialog', 'drawer'].includes(schema?.type) &&
!schema?.component && !schema?.component &&
@ -216,18 +220,26 @@ export function renderChild(
// 所以这里先根据 type 来处理一下 // 所以这里先根据 type 来处理一下
// 等后续把状态处理再抽一层,可以把此处放到 SchemaRenderer 里面去 // 等后续把状态处理再抽一层,可以把此处放到 SchemaRenderer 里面去
return ( return (
<StatusScopedSchemaRenderer <StatusScopedWrapper>
{({statusStore}) => (
<Comp
render={renderChild as any} render={renderChild as any}
{...props} {...props}
key={props.key ?? schema.key}
schema={schema} schema={schema}
propKey={schema.key} propKey={schema.key}
$path={`${prefix ? `${prefix}/` : ''}${(schema && schema.type) || ''}`} $path={`${prefix ? `${prefix}/` : ''}${
(schema && schema.type) || ''
}`}
statusStore={statusStore}
/> />
)}
</StatusScopedWrapper>
); );
} }
return ( return (
<SchemaRenderer <Comp
render={renderChild as any} render={renderChild as any}
{...props} {...props}
schema={schema} schema={schema}

View File

@ -86,7 +86,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
schema: any; schema: any;
path: string; path: string;
reaction: any; toDispose: Array<() => any> = [];
unbindEvent: (() => void) | undefined = undefined; unbindEvent: (() => void) | undefined = undefined;
isStatic: any = undefined; isStatic: any = undefined;
@ -99,7 +99,8 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
this.dispatchEvent = this.dispatchEvent.bind(this); this.dispatchEvent = this.dispatchEvent.bind(this);
// 监听statusStore更新 // 监听statusStore更新
this.reaction = reaction( this.toDispose.push(
reaction(
() => { () => {
const id = filter(props.schema.id, props.data); const id = filter(props.schema.id, props.data);
const name = filter(props.schema.name, props.data); const name = filter(props.schema.name, props.data);
@ -115,6 +116,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
}`; }`;
}, },
() => this.forceUpdate() () => this.forceUpdate()
)
); );
} }
@ -124,7 +126,8 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
} }
componentWillUnmount() { componentWillUnmount() {
this.reaction?.(); this.toDispose.forEach(fn => fn());
this.toDispose = [];
this.unbindEvent?.(); this.unbindEvent?.();
} }

View File

@ -8,80 +8,43 @@ export interface StatusScopedProps {
statusStore: IStatusStore; statusStore: IStatusStore;
} }
export interface StatusScopedWrapperProps {
children: (props: {statusStore: IStatusStore}) => JSX.Element;
}
export function StatusScopedWrapper({children}: StatusScopedWrapperProps) {
const store = React.useMemo(() => StatusStore.create({}), []);
React.useEffect(() => {
return () => {
destroy(store);
};
}, []);
return children({statusStore: store});
}
export function StatusScoped< export function StatusScoped<
T extends React.ComponentType<React.ComponentProps<T> & StatusScopedProps> T extends React.ComponentType<React.ComponentProps<T> & StatusScopedProps>
>(ComposedComponent: T) { >(ComposedComponent: T) {
type OuterProps = JSX.LibraryManagedAttributes< const wrapped = (
props: JSX.LibraryManagedAttributes<
T, T,
Omit<React.ComponentProps<T>, keyof StatusScopedProps> Omit<React.ComponentProps<T>, keyof StatusScopedProps>
> & {}; > & {},
ref: any
const result = hoistNonReactStatic( ) => {
class extends React.Component<OuterProps> {
static displayName = `StatusScoped(${
ComposedComponent.displayName || ComposedComponent.name
})`;
static ComposedComponent = ComposedComponent as React.ComponentType<T>;
store?: IStatusStore;
constructor(props: OuterProps) {
super(props);
this.childRef = this.childRef.bind(this);
this.getWrappedInstance = this.getWrappedInstance.bind(this);
this.store = StatusStore.create({});
}
ref: any;
childRef(ref: any) {
while (ref && ref.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
this.ref = ref;
}
getWrappedInstance() {
return this.ref;
}
componentWillUnmount(): void {
this.store && destroy(this.store);
delete this.store;
}
render() {
const injectedProps: {
statusStore: IStatusStore;
} = {
statusStore: this.store!
};
const refConfig =
ComposedComponent.prototype?.isReactComponent ||
(ComposedComponent as any).$$typeof ===
Symbol.for('react.forward_ref')
? {ref: this.childRef}
: {forwardedRef: this.childRef};
return ( return (
<StatusScopedWrapper>
{({statusStore}) => (
<ComposedComponent <ComposedComponent
{...(this.props as JSX.LibraryManagedAttributes< {...(props as any)}
T, statusStore={statusStore}
React.ComponentProps<T> ref={ref}
> as any)}
{...injectedProps}
{...refConfig}
/> />
)}
</StatusScopedWrapper>
); );
}
},
ComposedComponent
);
return result as typeof result & {
ComposedComponent: T;
}; };
return React.forwardRef(wrapped as any) as typeof wrapped;
} }

View File

@ -163,6 +163,11 @@ export interface RendererEnv {
action: ICmptAction, action: ICmptAction,
event: RendererEvent<any, any> event: RendererEvent<any, any>
) => Promise<void | boolean>; ) => Promise<void | boolean>;
/**
*
*/
SchemaRenderer?: React.ComponentType<any>;
} }
export const EnvContext = React.createContext<RendererEnv | void>(undefined); export const EnvContext = React.createContext<RendererEnv | void>(undefined);

View File

@ -108,7 +108,10 @@ import {
} from './utils/index'; } from './utils/index';
import type {OnEventProps} from './utils/index'; import type {OnEventProps} from './utils/index';
import {valueMap as styleMap} from './utils/style-helper'; import {valueMap as styleMap} from './utils/style-helper';
import {RENDERER_TRANSMISSION_OMIT_PROPS} from './SchemaRenderer'; import {
RENDERER_TRANSMISSION_OMIT_PROPS,
SchemaRenderer
} from './SchemaRenderer';
import type {IItem} from './store/list'; import type {IItem} from './store/list';
import CustomStyle from './components/CustomStyle'; import CustomStyle from './components/CustomStyle';
import {StatusScoped} from './StatusScoped'; import {StatusScoped} from './StatusScoped';
@ -207,7 +210,8 @@ export {
CustomStyle, CustomStyle,
enableDebug, enableDebug,
disableDebug, disableDebug,
envOverwrite envOverwrite,
SchemaRenderer
}; };
export function render( export function render(

View File

@ -262,7 +262,7 @@ export interface fetcherResult {
[propName: string]: any; // 为了兼容其他返回格式 [propName: string]: any; // 为了兼容其他返回格式
}; };
status: number; status: number;
headers: object; headers?: object;
} }
export interface fetchOptions { export interface fetchOptions {

View File

@ -38,7 +38,7 @@ import {BadgeObject} from 'amis-ui';
import {RemoteOptionsProps, withRemoteConfig} from 'amis-ui'; import {RemoteOptionsProps, withRemoteConfig} from 'amis-ui';
import {Spinner, Menu} from 'amis-ui'; import {Spinner, Menu} from 'amis-ui';
import {ScopedContext, IScopedContext} from 'amis-core'; import {ScopedContext, IScopedContext} from 'amis-core';
import type {NavigationItem} from 'amis-ui/lib/components/menu'; import type {NavigationItem} from 'amis-ui/lib/components/menu/index';
import type {MenuItemProps} from 'amis-ui/lib/components/menu/MenuItem'; import type {MenuItemProps} from 'amis-ui/lib/components/menu/MenuItem';
import type {Payload} from 'amis-core'; import type {Payload} from 'amis-core';

View File

@ -37,6 +37,7 @@ import {IIRendererStore} from 'amis-core';
import type {ListenerAction} from 'amis-core'; import type {ListenerAction} from 'amis-core';
import type {ScopedComponentType} from 'amis-core'; import type {ScopedComponentType} from 'amis-core';
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
import {isAlive} from 'mobx-state-tree';
export const eventTypes = [ export const eventTypes = [
/* 初始化时执行,默认 */ /* 初始化时执行,默认 */
@ -529,6 +530,9 @@ export default class Service extends React.Component<ServiceProps> {
// 保存 ajax 请求的时候返回时数据部分。 // 保存 ajax 请求的时候返回时数据部分。
const data = result?.hasOwnProperty('ok') ? result.data ?? {} : result; const data = result?.hasOwnProperty('ok') ? result.data ?? {} : result;
const {onBulkChange, dispatchEvent, store, formStore} = this.props; const {onBulkChange, dispatchEvent, store, formStore} = this.props;
if (!isAlive(store)) {
return;
}
dispatchEvent?.( dispatchEvent?.(
'fetchInited', 'fetchInited',