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

This commit is contained in:
liaoxuezhi 2024-10-30 14:37:36 +08:00
parent cd5865614a
commit 228cf0daaa
9 changed files with 125 additions and 106 deletions

View File

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

View File

@ -87,7 +87,7 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
schema: any;
path: string;
reaction: any;
toDispose: Array<() => any> = [];
unbindEvent: (() => void) | undefined = undefined;
isStatic: any = undefined;
@ -100,27 +100,30 @@ export class SchemaRenderer extends React.Component<SchemaRendererProps, any> {
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()
this.toDispose.push(
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()
)
);
}
componentWillUnmount() {
this.reaction?.();
this.toDispose.forEach(fn => fn());
this.toDispose = [];
this.unbindEvent?.();
}

View File

@ -8,80 +8,43 @@ export interface StatusScopedProps {
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<
T extends React.ComponentType<React.ComponentProps<T> & StatusScopedProps>
>(ComposedComponent: T) {
type OuterProps = JSX.LibraryManagedAttributes<
T,
Omit<React.ComponentProps<T>, keyof StatusScopedProps>
> & {};
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 (
const wrapped = (
props: JSX.LibraryManagedAttributes<
T,
Omit<React.ComponentProps<T>, keyof StatusScopedProps>
> & {},
ref: any
) => {
return (
<StatusScopedWrapper>
{({statusStore}) => (
<ComposedComponent
{...(this.props as JSX.LibraryManagedAttributes<
T,
React.ComponentProps<T>
> as any)}
{...injectedProps}
{...refConfig}
{...(props as any)}
statusStore={statusStore}
ref={ref}
/>
);
}
},
ComposedComponent
);
return result as typeof result & {
ComposedComponent: T;
)}
</StatusScopedWrapper>
);
};
return React.forwardRef(wrapped as any) as typeof wrapped;
}

View File

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

View File

@ -108,7 +108,10 @@ import {
} from './utils/index';
import type {OnEventProps} from './utils/index';
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 CustomStyle from './components/CustomStyle';
import {StatusScoped} from './StatusScoped';
@ -207,7 +210,8 @@ export {
CustomStyle,
enableDebug,
disableDebug,
envOverwrite
envOverwrite,
SchemaRenderer
};
export function render(

View File

@ -227,7 +227,11 @@ export interface ApiObject extends BaseApiObject {
operationName?: string;
body?: PlainObject;
query?: PlainObject;
mockResponse?: PlainObject;
mockResponse?: {
status: number;
data?: any;
delay?: number;
};
adaptor?: (
payload: object,
response: fetcherResult,
@ -263,7 +267,7 @@ export interface fetcherResult {
[propName: string]: any; // 为了兼容其他返回格式
};
status: number;
headers: object;
headers?: object;
}
export interface fetchOptions {

View File

@ -471,6 +471,14 @@ export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
return payload;
}
function lazyResolve<T = any>(value: T, waitFor = 1000) {
return new Promise<T>(resolve => {
setTimeout(() => {
resolve(value);
}, waitFor);
});
}
export function wrapFetcher(
fn: (config: FetcherConfig) => Promise<fetcherResult>,
tracker?: (eventTrack: EventTrack, data: any) => void
@ -540,7 +548,24 @@ export function wrapFetcher(
// 如果发送适配器中设置了 mockResponse
// 则直接跳过请求发送
if (api.mockResponse) {
return wrapAdaptor(Promise.resolve(api.mockResponse) as any, api, data);
console.debug(
`fetch api ${api.url}${
api.data
? `?${
typeof api.data === 'string'
? api.data
: qsstringify(api.data, api.qsOptions)
}`
: ''
} with mock response`,
api.mockResponse,
api
);
return wrapAdaptor(
lazyResolve(api.mockResponse, api.mockResponse?.delay ?? 100),
api,
data
);
}
if (!isValidApi(api.url)) {

View File

@ -38,7 +38,7 @@ import {BadgeObject} from 'amis-ui';
import {RemoteOptionsProps, withRemoteConfig} from 'amis-ui';
import {Spinner, Menu} from 'amis-ui';
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 {Payload} from 'amis-core';

View File

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