Merge branch feat/jsFunctionScope into develop

Title: feat: JSFunction 表达式支持 scope 值 

Link: https://code.aone.alibaba-inc.com/ali-lowcode/ali-lowcode-engine/codereview/8088102
This commit is contained in:
lihao.ylh 2022-03-23 19:48:43 +08:00
commit b5ed75a22d
11 changed files with 1967 additions and 10 deletions

View File

@ -199,6 +199,8 @@ exports[`React Renderer render basic case 1`] = `
maxLength={null}
onBlur={[Function]}
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="请选择"
@ -296,6 +298,8 @@ exports[`React Renderer render basic case 1`] = `
maxLength={null}
onBlur={[Function]}
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="请选择"
@ -378,6 +382,8 @@ exports[`React Renderer render basic case 1`] = `
maxLength={null}
onBlur={[Function]}
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
readOnly={false}
@ -826,6 +832,8 @@ exports[`React Renderer render basic case 1`] = `
maxLength={null}
onBlur={[Function]}
onChange={[Function]}
onCompositionEnd={[Function]}
onCompositionStart={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
readOnly={false}

View File

@ -0,0 +1,6 @@
{
"plugins": [
"build-plugin-component",
"@ali/lowcode-test-mate/plugin/index.ts"
]
}

View File

@ -0,0 +1,20 @@
const esModules = ['@recore/obx-react'].join('|');
module.exports = {
// transform: {
// '^.+\\.[jt]sx?$': 'babel-jest',
// // '^.+\\.(ts|tsx)$': 'ts-jest',
// // '^.+\\.(js|jsx)$': 'babel-jest',
// },
// testMatch: ['(/tests?/.*(test))\\.[jt]s$'],
transformIgnorePatterns: [
`/node_modules/(?!${esModules})/`,
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
],
};

View File

@ -10,8 +10,13 @@
"es"
],
"scripts": {
"test": "build-scripts test --config build.test.json",
"build": "build-scripts build --skip-demo"
},
"repository": {
"type": "git",
"url": "git@gitlab.alibaba-inc.com:ali-lowcode/ali-lowcode-engine.git"
},
"dependencies": {
"@ali/b3-one": "^0.0.17",
"@ali/bzb-request": "^2.6.0-beta.13",

View File

@ -32,7 +32,7 @@ import { compWrapper } from '../hoc';
import { IComponentConstruct, IComponentHoc, leafWrapper } from '../hoc/leaf';
import logger from '../utils/logger';
export default function baseRenererFactory() {
export default function baseRendererFactory() {
const { BaseRenderer: customBaseRenderer } = adapter.getRenderers();
if (customBaseRenderer) {
@ -712,7 +712,7 @@ export default function baseRenererFactory() {
) {
return checkProps(props);
}
if (isJSExpression(props)) {
if (isJSExpression(props) || isJSFunction(props)) {
props = parseExpression(props, scope);
// 只有当变量解析出来为模型结构的时候才会继续解析
if (!isSchema(props) && !isJSSlot(props)) return checkProps(props);

View File

@ -1,8 +1,7 @@
import { BuiltinSimulatorHost } from '@ali/lowcode-designer';
import { baseRendererFactory } from '../renderer';
import baseRenererFactory from '../renderer/base';
export type IBaseRenderer = ReturnType<typeof baseRenererFactory>;
export type IBaseRenderer = ReturnType<typeof baseRendererFactory>;
export type IBaseRendererInstance = InstanceType<ReturnType<typeof baseRendererFactory>>;
export interface IProps {
@ -28,7 +27,7 @@ export interface IProps {
export interface IState {
engineRenderError?: boolean;
error?: Error
error?: Error;
onCompGetRef: (schema: ISchema, ref: any) => void;
onCompGetCtx: (schema: ISchema, ref: any) => void;
customCreateElement: (...args: any) => any;
@ -57,7 +56,7 @@ export interface ComponentModel {
export interface ISchema {
componentName: string;
props: any;
children: ComponentModel[]
children: ComponentModel[];
dataSource?: any;
methods?: any;
lifeCycles?: any;
@ -68,7 +67,7 @@ export interface IInfo {
schema: ISchema;
Comp: any;
componentInfo?: any;
componentChildren?: any
componentChildren?: any;
}
export interface JSExpression {
@ -113,9 +112,9 @@ export interface IRendererModules {
BaseRenderer?: new(...args: any) => IRenderer;
PageRenderer: any;
ComponentRenderer: any;
BlockRenderer?: any,
AddonRenderer?: any,
TempRenderer?: any,
BlockRenderer?: any;
AddonRenderer?: any;
TempRenderer?: any;
DivRenderer?: any;
}

View File

@ -0,0 +1,567 @@
export default {
componentName: 'Page',
id: 'node_dockcviv8fo1',
props: {
ref: 'outterView',
autoLoading: true,
style: {
padding: '0 5px 0 5px',
},
},
fileName: 'test',
dataSource: {
list: [],
},
state: {
text: 'outter',
isShowDialog: false,
},
css: 'body {font-size: 12px;} .botton{width:100px;color:#ff00ff}',
lifeCycles: {
componentDidMount: {
type: 'JSFunction',
value: "function() {\n console.log('did mount');\n }",
},
componentWillUnmount: {
type: 'JSFunction',
value: "function() {\n console.log('will umount');\n }",
},
},
methods: {
testFunc: {
type: 'JSFunction',
value: "function() {\n console.log('test func');\n }",
},
onClick: {
type: 'JSFunction',
value: 'function() {\n this.setState({\n isShowDialog: true\n })\n }',
},
closeDialog: {
type: 'JSFunction',
value: 'function() {\n this.setState({\n isShowDialog: false\n })\n }',
},
},
children: [
{
componentName: 'Box',
id: 'node_dockcy8n9xed',
props: {
style: {
backgroundColor: 'rgba(31,56,88,0.1)',
padding: '12px 12px 12px 12px',
},
},
children: [
{
componentName: 'Box',
id: 'node_dockcy8n9xee',
props: {
style: {
padding: '12px 12px 12px 12px',
backgroundColor: '#ffffff',
},
},
children: [
{
componentName: 'Breadcrumb',
id: 'node_dockcy8n9xef',
props: {
prefix: 'next-',
maxNode: 100,
component: 'nav',
},
children: [
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xeg',
props: {
prefix: 'next-',
children: '首页',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xei',
props: {
prefix: 'next-',
children: '品质中台',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xek',
props: {
prefix: 'next-',
children: '商家品质页面管理',
},
},
{
componentName: 'Breadcrumb.Item',
id: 'node_dockcy8n9xem',
props: {
prefix: 'next-',
children: '质检知识条配置',
},
},
],
},
],
},
{
componentName: 'Box',
id: 'node_dockcy8n9xeo',
props: {
style: {
marginTop: '12px',
backgroundColor: '#ffffff',
},
},
children: [
{
componentName: 'Form',
id: 'node_dockcy8n9xep',
props: {
inline: true,
style: {
marginTop: '12px',
marginRight: '12px',
marginLeft: '12px',
},
__events: [],
},
children: [
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xeq',
props: {
style: {
marginBottom: '0',
},
label: '类目名:',
},
children: [
{
componentName: 'Select',
id: 'node_dockcy8n9xer',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
style: {
width: '150px',
},
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xes',
props: {
style: {
marginBottom: '0',
},
label: '项目类型:',
},
children: [
{
componentName: 'Select',
id: 'node_dockcy8n9xet',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
style: {
width: '200px',
},
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockcy8n9xeu',
props: {
style: {
marginBottom: '0',
},
label: '项目 ID',
},
children: [
{
componentName: 'Input',
id: 'node_dockcy8n9xev',
props: {
hasBorder: true,
size: 'medium',
autoComplete: 'off',
style: {
width: '200px',
},
},
},
],
},
{
componentName: 'Button.Group',
id: 'node_dockcy8n9xew',
props: {},
children: [
{
componentName: 'Button',
id: 'node_dockcy8n9xex',
props: {
type: 'primary',
style: {
margin: '0 5px 0 5px',
},
htmlType: 'submit',
children: '搜索',
},
},
{
componentName: 'Button',
id: 'node_dockcy8n9xe10',
props: {
type: 'normal',
style: {
margin: '0 5px 0 5px',
},
htmlType: 'reset',
children: '清空',
},
},
],
},
],
},
],
},
{
componentName: 'Box',
id: 'node_dockcy8n9xe1f',
props: {
style: {
backgroundColor: '#ffffff',
paddingBottom: '24px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
},
children: [
{
componentName: 'Button',
id: 'node_dockd5nrh9p4',
props: {
type: 'primary',
size: 'medium',
htmlType: 'button',
component: 'button',
children: '新建配置',
style: {},
__events: [
{
type: 'componentEvent',
name: 'onClick',
relatedEventName: 'onClick',
},
],
onClick: {
type: 'JSFunction',
value: 'function(){ this.onClick() }',
},
},
},
],
},
{
componentName: 'Box',
id: 'node_dockd5nrh9p5',
props: {},
children: [
{
componentName: 'Table',
id: 'node_dockjielosj1',
props: {
showMiniPager: true,
showActionBar: true,
actionBar: [
{
title: '新增',
type: 'primary',
},
{
title: '编辑',
},
],
columns: [
{
dataKey: 'name',
width: 200,
align: 'center',
title: '姓名',
editType: 'text',
},
{
dataKey: 'age',
width: 200,
align: 'center',
title: '年龄',
},
{
dataKey: 'email',
width: 200,
align: 'center',
title: '邮箱',
},
],
data: [
{
name: '王小',
id: '1',
age: 15000,
email: 'aaa@abc.com',
},
{
name: '王中',
id: '2',
age: 25000,
email: 'bbb@abc.com',
},
{
name: '王大',
id: '3',
age: 35000,
email: 'ccc@abc.com',
},
],
actionTitle: '操作',
actionWidth: 180,
actionType: 'link',
actionFixed: 'right',
actionHidden: false,
maxWebShownActionCount: 2,
actionColumn: [
{
title: '编辑',
callback: {
type: 'JSFunction',
value: '(rowData, action, table) => {\n return table.editRow(rowData).then((row) => {\n console.log(row);\n });\n }',
},
device: [
'desktop',
],
},
{
title: '保存',
callback: {
type: 'JSFunction',
value: '(rowData, action, table) => { \nreturn table.saveRow(rowData).then((row) => { \nconsole.log(row); \n}); \n}',
},
mode: 'EDIT',
},
],
},
},
{
componentName: 'Box',
id: 'node_dockd5nrh9pg',
props: {
style: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
},
},
children: [
{
componentName: 'Pagination',
id: 'node_dockd5nrh9pf',
props: {
prefix: 'next-',
type: 'normal',
shape: 'normal',
size: 'medium',
defaultCurrent: 1,
total: 100,
pageShowCount: 5,
pageSize: 10,
pageSizePosition: 'start',
showJump: true,
style: {},
},
},
],
},
],
},
],
},
{
componentName: 'Dialog',
id: 'node_dockcy8n9xe1h',
props: {
prefix: 'next-',
footerAlign: 'right',
footerActions: [
'ok',
'cancel',
],
closeable: 'esc,close',
hasMask: true,
align: 'cc cc',
minMargin: 40,
visible: {
type: 'JSExpression',
value: 'this.state.isShowDialog',
},
title: '标题',
events: [],
__events: [
{
type: 'componentEvent',
name: 'onCancel',
relatedEventName: 'closeDialog',
},
{
type: 'componentEvent',
name: 'onClose',
relatedEventName: 'closeDialog',
},
{
type: 'componentEvent',
name: 'onOk',
relatedEventName: 'testFunc',
},
],
onCancel: {
type: 'JSFunction',
value: 'function(){ this.closeDialog() }',
},
onClose: {
type: 'JSFunction',
value: 'function(){ this.closeDialog() }',
},
onOk: {
type: 'JSFunction',
value: 'function(){ this.testFunc() }',
},
},
children: [
{
componentName: 'Form',
id: 'node_dockd5nrh9pi',
props: {
inline: false,
labelAlign: 'top',
labelTextAlign: 'right',
size: 'medium',
},
children: [
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pj',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9pk',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pl',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9pm',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pn',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
asterisk: true,
},
children: [
{
componentName: 'Select',
id: 'node_dockd5nrh9po',
props: {
mode: 'single',
hasArrow: true,
cacheValue: true,
},
},
],
},
{
componentName: 'Form.Item',
id: 'node_dockd5nrh9pp',
props: {
style: {
marginBottom: '0',
minWidth: '200px',
minHeight: '28px',
},
label: '商品类目',
},
children: [
{
componentName: 'Input',
id: 'node_dockd5nrh9pr',
props: {
hasBorder: true,
size: 'medium',
autoComplete: 'off',
},
},
],
},
],
},
],
},
{
componentName: 'ErrorComponent',
id: 'node_dockd5nrh9pr',
props: {
name: 'error',
},
},
],
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,241 @@
import React from 'react';
import renderer from 'react-test-renderer';
import schema from '../fixtures/schema/basic';
import '../utils/mock-react-render';
import rendererFactory from '../../src/renderer/renderer';
import components from '../utils/components';
const Renderer = rendererFactory();
function getComp(schema, comp = null): Promise<{
component,
inst,
}> {
return new Promise((resolve, reject) => {
const component = renderer.create(
// @ts-ignore
<Renderer
schema={schema}
components={components}
/>);
const componentInstance = component.root;
setTimeout(() => {
resolve({
inst: comp ? componentInstance.findAllByType(comp) : null,
component,
});
}, 20);
})
}
beforeEach(() => {
});
let componentSnapshot;
afterEach(() => {
if (componentSnapshot) {
let tree = componentSnapshot.toJSON();
expect(tree).toMatchSnapshot();
componentSnapshot = null;
}
});
describe('Base Render', () => {
it('renderComp', () => {
const content = (
// @ts-ignore
<Renderer
schema={schema}
components={components}
/>);
const tree = renderer.create(content).toJSON();
expect(tree).toMatchSnapshot();
});
});
describe('JSExpression', () => {
it('base props', (done) => {
const schema = {
componentName: 'Page',
props: {},
children: [
{
componentName: "Div",
props: {
className: 'div-ut',
text: "123",
visible: true,
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst[0].props.text).toBe('123');
expect(inst[0].props.visible).toBeTruthy();
componentSnapshot = component;
done();
});
});
it('JSExpression props', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
props: {
className: "div-ut",
visible: {
type: 'JSExpression',
value: 'this.state.isShowDialog',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(inst[0].props.visible).toBeTruthy();
componentSnapshot = component;
done();
});
});
it('JSExpression props with loop', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
loop: [
{
name: '1',
},
{
name: '2'
}
],
props: {
className: "div-ut",
name1: {
type: 'JSExpression',
value: 'this.item.name',
},
name2: {
type: 'JSExpression',
value: 'item.name',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
// expect(inst[0].props.visible).toBeTruthy();
expect(inst.length).toEqual(2);
[1, 2].forEach((i) => {
expect(inst[0].props[`name${i}`]).toBe('1');
expect(inst[1].props[`name${i}`]).toBe('2');
})
componentSnapshot = component;
done();
});
});
it('JSFunction props with loop', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
loop: [
{
name: '1',
},
{
name: '2'
}
],
props: {
className: "div-ut",
onClick1: {
type: 'JSFunction',
value: '() => this.item.name',
},
onClick2: {
type: 'JSFunction',
value: 'function(){ return this.item.name }',
},
onClick3: {
type: 'JSFunction',
value: 'function(){ return item.name }',
},
onClick4: {
type: 'JSFunction',
value: '() => item.name',
}
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
// expect(inst[0].props.visible).toBeTruthy();
expect(inst.length).toEqual(2);
[1, 2, 3, 4].forEach((i) => {
expect(inst[0].props[`onClick${i}`]()).toBe('1');
expect(inst[1].props[`onClick${i}`]()).toBe('2');
})
componentSnapshot = component;
done();
});
});
it('JSFunction props', (done) => {
const schema = {
componentName: 'Page',
props: {},
state: {
isShowDialog: true,
},
children: [
{
componentName: "Div",
props: {
className: "div-ut",
onClick: {
type: 'JSExpression',
value: '() => this.state.isShowDialog',
},
}
}
]
};
getComp(schema, components.Div).then(({ component, inst }) => {
expect(!!inst[0].props.onClick).toBeTruthy();
expect(inst[0].props.onClick()).toBeTruthy();
componentSnapshot = component;
done();
});;
});
})

View File

@ -0,0 +1,22 @@
import { Box, Breadcrumb, Form, Select, Input, Button, Table, Pagination, Dialog } from '@alifd/next';
const Div = (props) => (<div {...props}>{props.children}</div>);
const components = {
Box,
Breadcrumb,
'Breadcrumb.Item': Breadcrumb.Item,
Form,
'Form.Item': Form.Item,
Select,
Input,
Button,
'Button.Group': Button.Group,
Table,
Pagination,
Dialog,
ErrorComponent: Select,
Div,
};
export default components;

View File

@ -0,0 +1,66 @@
import React, { Component, PureComponent, createElement, createContext, forwardRef, ReactInstance, ContextType } from 'react';
import ReactDOM from 'react-dom';
import {
adapter,
pageRendererFactory,
componentRendererFactory,
blockRendererFactory,
addonRendererFactory,
tempRendererFactory,
rendererFactory,
types,
} from '../../src';
import ConfigProvider from '@alifd/next/lib/config-provider';
window.React = React;
(window as any).ReactDom = ReactDOM;
adapter.setRuntime({
Component,
PureComponent,
createContext,
createElement,
forwardRef,
findDOMNode: ReactDOM.findDOMNode,
});
adapter.setRenderers({
PageRenderer: pageRendererFactory(),
ComponentRenderer: componentRendererFactory(),
BlockRenderer: blockRendererFactory(),
AddonRenderer: addonRendererFactory(),
TempRenderer: tempRendererFactory(),
DivRenderer: blockRendererFactory(),
});
adapter.setConfigProvider(ConfigProvider);
function factory() {
const Renderer = rendererFactory();
return class ReactRenderer extends Renderer implements Component {
readonly props: types.IProps;
context: ContextType<any>;
setState: (
state: types.IState,
callback?: () => void,
) => void;
forceUpdate: (callback?: () => void) => void;
refs: {
[key: string]: ReactInstance,
};
constructor(props: types.IProps, context: ContextType<any>) {
super(props, context);
}
isValidComponent(obj: any) {
return obj?.prototype?.isReactComponent || obj?.prototype instanceof Component;
}
};
}
export default factory();