mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
feat: 渲染器包裹支持外层自定义 (#11138)
This commit is contained in:
parent
cd5865614a
commit
228cf0daaa
@ -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}
|
||||
|
@ -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?.();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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)) {
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user