;
92 | }
93 |
94 | export interface TangoComponentProps {
95 | /**
96 | * 组件 ID (兼容旧版设计)
97 | */
98 | id?: string;
99 | /**
100 | * 组件 ID,同时用于页面内的状态访问路径
101 | */
102 | tid?: string;
103 | }
104 |
105 | const registerEmpty = () => ({});
106 |
107 | // TODO:支持本地组件的属性配置设置
108 | export function defineComponent(
109 | BaseComponent: React.ComponentType
,
110 | options?: DefineComponentConfig,
111 | ) {
112 | const displayName =
113 | options?.name || BaseComponent.displayName || BaseComponent.name || 'TangoComponent';
114 | const designerConfig = options?.designerConfig || {};
115 |
116 | const isFC = isFunctionComponent(BaseComponent);
117 | const isDesignMode = isInTangoDesignMode();
118 |
119 | // 这里包上 view ,能够响应 model 变化
120 | const InnerModelComponent = view((props: P & TangoModelComponentProps) => {
121 | const ref = useRef();
122 |
123 | const stateConfig = options?.registerState || {};
124 |
125 | const getPageStates = stateConfig.getInitStates || registerEmpty;
126 | const { tid, innerRef, ...rest } = props;
127 |
128 | const setPageState = (nextState: Dict) => {
129 | tangoBoot.setPageState(tid, nextState);
130 | };
131 |
132 | const getPageState = () => {
133 | return tangoBoot.getPageState(tid);
134 | };
135 |
136 | useEffect(() => {
137 | if (tid) {
138 | const customStates = getPageStates({ getPageState, setPageState }, props, ref.current);
139 | tangoBoot.setPageState(tid, {
140 | ...customStates,
141 | });
142 | }
143 | return () => {
144 | if (tid) {
145 | tangoBoot.clearPageState(tid);
146 | }
147 | };
148 | }, [tid]);
149 |
150 | const override: Dict = {};
151 |
152 | let userTriggerProps = {};
153 | if (tid) {
154 | userTriggerProps = stateConfig.getTriggerProps?.({ getPageState, setPageState });
155 | const handlerKeys = Object.keys(userTriggerProps);
156 | if (handlerKeys.length) {
157 | handlerKeys.forEach((key) => {
158 | // FIXME: 应该只需要合并 function 类型的属性,其他属性不需要合并
159 | if (props[key] || override[key]) {
160 | userTriggerProps[key] = callAll(userTriggerProps[key], override[key], props[key]);
161 | }
162 | });
163 | }
164 | }
165 |
166 | return (
167 |
173 | );
174 | });
175 |
176 | // TIP: view 不支持 forwardRef,这里包一层,包到内部组件去消费,外层支持访问到原始的 ref,避免与原始代码产生冲突
177 | const TangoComponent = forwardRef((props, ref) => {
178 | const { tid } = props;
179 | const refs = isFC ? undefined : ref;
180 |
181 | let renderComponent: (defaultProps?: P) => React.ReactElement;
182 | if (options?.registerState && tid) {
183 | renderComponent = (defaultProps: P) =>
184 | React.createElement(InnerModelComponent, { innerRef: refs, ...defaultProps, ...props });
185 | } else {
186 | renderComponent = (defaultProps: P) =>
187 | React.createElement(BaseComponent, { ref: refs, ...defaultProps, ...props });
188 | }
189 |
190 | if (isDesignMode) {
191 | // design mode
192 | const overrideProps = designerConfig.defaultProps;
193 | const ret = renderComponent(overrideProps as P);
194 |
195 | const designerProps = {
196 | draggable: designerConfig.draggable ?? true,
197 | [SLOT.id]: tid,
198 | [SLOT.dnd]: props[SLOT.dnd],
199 | };
200 |
201 | if (designerConfig.render) {
202 | // 自定义渲染设计器样式
203 | return designerConfig.render({ designerProps, originalProps: props, children: ret });
204 | }
205 |
206 | if (designerConfig.hasWrapper) {
207 | return (
208 |
214 | {ret}
215 |
216 | );
217 | } else {
218 | return renderComponent({
219 | ...overrideProps,
220 | ...designerProps,
221 | } as any);
222 | }
223 | } else {
224 | // normal mode
225 | return renderComponent();
226 | }
227 | });
228 |
229 | hoistNonReactStatics(TangoComponent, BaseComponent);
230 | TangoComponent.displayName = `defineComponent(${displayName})`;
231 |
232 | return TangoComponent;
233 | }
234 |
235 | interface DndBoxProps extends React.ComponentPropsWithoutRef<'div'> {
236 | name?: string;
237 | display?: 'block' | 'inline-block' | 'inline';
238 | }
239 |
240 | function DndBox({ name, display, children, style: styleProp, ...rest }: DndBoxProps) {
241 | const style = {
242 | display,
243 | minHeight: 4,
244 | ...styleProp,
245 | };
246 | return (
247 |
248 | {children}
249 |
250 | );
251 | }
252 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For a detailed explanation regarding each configuration property, visit:
3 | * https://jestjs.io/docs/configuration
4 | */
5 |
6 | /** @type {import('jest').Config} */
7 | const config = {
8 | // All imported modules in your tests should be mocked automatically
9 | // automock: false,
10 |
11 | // Stop running tests after `n` failures
12 | // bail: 0,
13 |
14 | // The directory where Jest should store its cached dependency information
15 | // cacheDirectory: "/private/var/folders/q_/7k4h88k50pn0p6423z7smp9r0000gn/T/jest_dx",
16 |
17 | // Automatically clear mock calls, instances, contexts and results before every test
18 | clearMocks: true,
19 |
20 | // Indicates whether the coverage information should be collected while executing the test
21 | collectCoverage: true,
22 |
23 | // An array of glob patterns indicating a set of files for which coverage information should be collected
24 | // collectCoverageFrom: undefined,
25 |
26 | // The directory where Jest should output its coverage files
27 | coverageDirectory: 'coverage',
28 |
29 | // An array of regexp pattern strings used to skip coverage collection
30 | // coveragePathIgnorePatterns: [
31 | // "/node_modules/"
32 | // ],
33 |
34 | // Indicates which provider should be used to instrument code for coverage
35 | // coverageProvider: "babel",
36 |
37 | // A list of reporter names that Jest uses when writing coverage reports
38 | // coverageReporters: [
39 | // "json",
40 | // "text",
41 | // "lcov",
42 | // "clover"
43 | // ],
44 |
45 | // An object that configures minimum threshold enforcement for coverage results
46 | // coverageThreshold: undefined,
47 |
48 | // A path to a custom dependency extractor
49 | // dependencyExtractor: undefined,
50 |
51 | // Make calling deprecated APIs throw helpful error messages
52 | // errorOnDeprecated: false,
53 |
54 | // The default configuration for fake timers
55 | // fakeTimers: {
56 | // "enableGlobally": false
57 | // },
58 |
59 | // Force coverage collection from ignored files using an array of glob patterns
60 | // forceCoverageMatch: [],
61 |
62 | // A path to a module which exports an async function that is triggered once before all test suites
63 | // globalSetup: undefined,
64 |
65 | // A path to a module which exports an async function that is triggered once after all test suites
66 | // globalTeardown: undefined,
67 |
68 | // A set of global variables that need to be available in all test environments
69 | // globals: {},
70 |
71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
72 | // maxWorkers: "50%",
73 |
74 | // An array of directory names to be searched recursively up from the requiring module's location
75 | // moduleDirectories: [
76 | // "node_modules"
77 | // ],
78 |
79 | // An array of file extensions your modules use
80 | // moduleFileExtensions: [
81 | // "js",
82 | // "mjs",
83 | // "cjs",
84 | // "jsx",
85 | // "ts",
86 | // "tsx",
87 | // "json",
88 | // "node"
89 | // ],
90 |
91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
92 | // moduleNameMapper: {},
93 |
94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
95 | // modulePathIgnorePatterns: [],
96 |
97 | // Activates notifications for test results
98 | // notify: false,
99 |
100 | // An enum that specifies notification mode. Requires { notify: true }
101 | // notifyMode: "failure-change",
102 |
103 | // A preset that is used as a base for Jest's configuration
104 | // preset: undefined,
105 |
106 | // Run tests from one or more projects
107 | // projects: undefined,
108 |
109 | // Use this configuration option to add custom reporters to Jest
110 | // reporters: undefined,
111 |
112 | // Automatically reset mock state before every test
113 | // resetMocks: false,
114 |
115 | // Reset the module registry before running each individual test
116 | // resetModules: false,
117 |
118 | // A path to a custom resolver
119 | // resolver: undefined,
120 |
121 | // Automatically restore mock state and implementation before every test
122 | // restoreMocks: false,
123 |
124 | // The root directory that Jest should scan for tests and modules within
125 | // rootDir: undefined,
126 |
127 | // A list of paths to directories that Jest should use to search for files in
128 | // roots: [
129 | // ""
130 | // ],
131 |
132 | // Allows you to use a custom runner instead of Jest's default test runner
133 | // runner: "jest-runner",
134 |
135 | // The paths to modules that run some code to configure or set up the testing environment before each test
136 | // setupFiles: [],
137 |
138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test
139 | // setupFilesAfterEnv: [],
140 |
141 | // The number of seconds after which a test is considered as slow and reported as such in the results.
142 | // slowTestThreshold: 5,
143 |
144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing
145 | // snapshotSerializers: [],
146 |
147 | // The test environment that will be used for testing
148 | testEnvironment: 'jsdom',
149 |
150 | // Options that will be passed to the testEnvironment
151 | // testEnvironmentOptions: {},
152 |
153 | // Adds a location field to test results
154 | // testLocationInResults: false,
155 |
156 | // The glob patterns Jest uses to detect test files
157 | testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
158 |
159 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
160 | // testPathIgnorePatterns: [
161 | // "/node_modules/"
162 | // ],
163 |
164 | // The regexp pattern or array of patterns that Jest uses to detect test files
165 | // testRegex: [],
166 |
167 | // This option allows the use of a custom results processor
168 | // testResultsProcessor: undefined,
169 |
170 | // This option allows use of a custom test runner
171 | // testRunner: "jest-circus/runner",
172 |
173 | // A map from regular expressions to paths to transformers
174 | // transform: undefined,
175 |
176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
177 | // transformIgnorePatterns: [
178 | // "/node_modules/",
179 | // "\\.pnp\\.[^\\/]+$"
180 | // ],
181 |
182 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
183 | // unmockedModulePathPatterns: undefined,
184 |
185 | // Indicates whether each individual test should be reported during the run
186 | // verbose: undefined,
187 |
188 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
189 | // watchPathIgnorePatterns: [],
190 |
191 | // Whether to use watchman for file crawling
192 | // watchman: true,
193 | };
194 |
195 | module.exports = config;
196 |
--------------------------------------------------------------------------------