-
Notifications
You must be signed in to change notification settings - Fork 8
/
defineComponent.tsx
251 lines (222 loc) · 6.25 KB
/
defineComponent.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import React, { forwardRef, useEffect, useRef } from 'react';
import { view } from '@risingstack/react-easy-state';
import {
Dict,
SLOT,
callAll,
hoistNonReactStatics,
isFunctionComponent,
isInTangoDesignMode,
} from '@music163/tango-helpers';
import tangoBoot from './global';
import { mergeRefs } from './helpers';
interface IPageStateHandlers {
getPageState: () => Dict;
setPageState: (stateValue: Dict) => void;
}
interface RegisterStateConfig {
/**
* 用户自定义的组件状态
* @param props
* @param instance
* @returns
*/
getInitStates?: (handlers: IPageStateHandlers, props: any, instance: any) => any;
/**
* 用户自定义的触发器行为
* @param handlers
* @returns
*/
getTriggerProps?: (handlers: IPageStateHandlers) => Dict;
}
interface DesignerRenderProps {
originalProps: Record<string, any>;
designerProps: Record<string, any>;
children: React.ReactElement;
}
interface DesignerConfig {
/**
* 是否可拖拽
*/
draggable?: boolean;
/**
* 是否有包裹容器
*/
hasWrapper?: boolean;
/**
* 容器自定义样式
*/
wrapperStyle?: React.CSSProperties;
/**
* 展示方式
*/
display?: DndBoxProps['display'];
/**
* 自定义渲染
*/
render?: (props: DesignerRenderProps) => React.ReactNode;
/**
* 注入给组件的默认属性
*/
defaultProps?: Record<string, any>;
}
interface DefineComponentConfig {
/**
* displayName
*/
name?: string;
/**
* 同步组件状态到 tango.page 上
*/
registerState?: RegisterStateConfig;
/**
* 组件在设计态配置
*/
designerConfig?: DesignerConfig;
}
interface TangoModelComponentProps extends TangoComponentProps {
/**
* 默认值
*/
defaultValue?: any;
/**
* 内部 ref
*/
innerRef?: React.ComponentRef<any>;
}
export interface TangoComponentProps {
/**
* 组件 ID (兼容旧版设计)
*/
id?: string;
/**
* 组件 ID,同时用于页面内的状态访问路径
*/
tid?: string;
}
const registerEmpty = () => ({});
// TODO:支持本地组件的属性配置设置
export function defineComponent<P = any>(
BaseComponent: React.ComponentType<P>,
options?: DefineComponentConfig,
) {
const displayName =
options?.name || BaseComponent.displayName || BaseComponent.name || 'TangoComponent';
const designerConfig = options?.designerConfig || {};
const isFC = isFunctionComponent(BaseComponent);
const isDesignMode = isInTangoDesignMode();
// 这里包上 view ,能够响应 model 变化
const InnerModelComponent = view((props: P & TangoModelComponentProps) => {
const ref = useRef();
const stateConfig = options?.registerState || {};
const getPageStates = stateConfig.getInitStates || registerEmpty;
const { tid, innerRef, ...rest } = props;
const setPageState = (nextState: Dict) => {
tangoBoot.setPageState(tid, nextState);
};
const getPageState = () => {
return tangoBoot.getPageState(tid);
};
useEffect(() => {
if (tid) {
const customStates = getPageStates({ getPageState, setPageState }, props, ref.current);
tangoBoot.setPageState(tid, {
...customStates,
});
}
return () => {
if (tid) {
tangoBoot.clearPageState(tid);
}
};
}, [tid]);
const override: Dict = {};
let userTriggerProps = {};
if (tid) {
userTriggerProps = stateConfig.getTriggerProps?.({ getPageState, setPageState });
const handlerKeys = Object.keys(userTriggerProps);
if (handlerKeys.length) {
handlerKeys.forEach((key) => {
// FIXME: 应该只需要合并 function 类型的属性,其他属性不需要合并
if (props[key] || override[key]) {
userTriggerProps[key] = callAll(userTriggerProps[key], override[key], props[key]);
}
});
}
}
return (
<BaseComponent
{...(rest as P)}
{...override}
{...userTriggerProps}
ref={mergeRefs(ref, innerRef)}
/>
);
});
// TIP: view 不支持 forwardRef,这里包一层,包到内部组件去消费,外层支持访问到原始的 ref,避免与原始代码产生冲突
const TangoComponent = forwardRef<unknown, P & TangoComponentProps>((props, ref) => {
const { tid } = props;
const refs = isFC ? undefined : ref;
let renderComponent: (defaultProps?: P) => React.ReactElement;
if (options?.registerState && tid) {
renderComponent = (defaultProps: P) =>
React.createElement(InnerModelComponent, { innerRef: refs, ...defaultProps, ...props });
} else {
renderComponent = (defaultProps: P) =>
React.createElement(BaseComponent, { ref: refs, ...defaultProps, ...props });
}
if (isDesignMode) {
// design mode
const overrideProps = designerConfig.defaultProps;
const ret = renderComponent(overrideProps as P);
const designerProps = {
draggable: designerConfig.draggable ?? true,
[SLOT.id]: tid,
[SLOT.dnd]: props[SLOT.dnd],
};
if (designerConfig.render) {
// 自定义渲染设计器样式
return designerConfig.render({ designerProps, originalProps: props, children: ret });
}
if (designerConfig.hasWrapper) {
return (
<DndBox
name={displayName}
display={designerConfig.display}
style={options.designerConfig?.wrapperStyle}
{...designerProps}
>
{ret}
</DndBox>
);
} else {
return renderComponent({
...overrideProps,
...designerProps,
} as any);
}
} else {
// normal mode
return renderComponent();
}
});
hoistNonReactStatics(TangoComponent, BaseComponent);
TangoComponent.displayName = `defineComponent(${displayName})`;
return TangoComponent;
}
interface DndBoxProps extends React.ComponentPropsWithoutRef<'div'> {
name?: string;
display?: 'block' | 'inline-block' | 'inline';
}
function DndBox({ name, display, children, style: styleProp, ...rest }: DndBoxProps) {
const style = {
display,
minHeight: 4,
...styleProp,
};
return (
<div className={`${name}-designer tango-dndBox`} style={style} {...rest}>
{children}
</div>
);
}