Match-id-c8f42259603ae600fa183076f7c3db48d32b9065
This commit is contained in:
commit
d087bf644f
|
@ -0,0 +1,7 @@
|
|||
## 0.0.8 (2022-07-08)
|
||||
### Features
|
||||
- 增加HorizonX提供状态管理能力
|
||||
### Bug Fixes
|
||||
- **core**: 修复局部更新场景下context计算错误
|
||||
### Code Refactoring
|
||||
- 重构事件机制,取消全量挂载事件,改为按需懒挂载
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"presets": [
|
||||
"@babel/react",
|
||||
"@babel/typescript",
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"modules": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
{
|
||||
"loose": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-transform-react-jsx",
|
||||
{
|
||||
"pragma": "Horizon.createElement",
|
||||
"pragmaFrag": "Horizon.Fragment"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
Horizon X antd demo:
|
||||
1. run `npm run build:watch` in root's `package.json`
|
||||
2. run `npm start` to run Horizon X antd
|
|
@ -0,0 +1,38 @@
|
|||
import Horizon from 'horizon';
|
||||
import { AppstoreOutlined } from '@ant-design/icons';
|
||||
import { Menu } from 'antd';
|
||||
|
||||
function getItem(label, key, icon, children, type) {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
const items = [
|
||||
getItem('sub2', 'sub2', <AppstoreOutlined />, [getItem('sub3', 'sub3', null, [getItem('sub4', 'sub4')])]),
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
const onClick = e => {
|
||||
console.log('click ', e);
|
||||
};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
onClick={onClick}
|
||||
style={{
|
||||
width: 256,
|
||||
}}
|
||||
defaultSelectedKeys={['sub2']}
|
||||
defaultOpenKeys={['sub2', 'sub3']}
|
||||
mode="inline"
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,73 @@
|
|||
import Horizon, { useState } from 'horizon';
|
||||
import {
|
||||
AppstoreOutlined,
|
||||
ContainerOutlined,
|
||||
MenuFoldOutlined,
|
||||
PieChartOutlined,
|
||||
DesktopOutlined,
|
||||
MailOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Menu } from 'antd';
|
||||
|
||||
function getItem(label, key, icon, children, type) {
|
||||
return {
|
||||
key,
|
||||
icon,
|
||||
children,
|
||||
label,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
const items = [
|
||||
getItem('选项1', '1', <PieChartOutlined />),
|
||||
getItem('选项2', '2', <DesktopOutlined />),
|
||||
getItem('选项3', '3', <ContainerOutlined />),
|
||||
getItem('分组1', 'sub1', <MailOutlined />, [
|
||||
getItem('选项5', '5'),
|
||||
getItem('选项6', '6'),
|
||||
getItem('选项7', '7'),
|
||||
getItem('选项8', '8'),
|
||||
]),
|
||||
getItem('分组2', 'sub2', <AppstoreOutlined />, [
|
||||
getItem('选项9', '9'),
|
||||
getItem('选项10', '10'),
|
||||
getItem('分组2-1', 'sub3', null, [getItem('选项11', '11'), getItem('选项12', '12')]),
|
||||
]),
|
||||
];
|
||||
|
||||
const App = () => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: 256,
|
||||
marginLeft: 32,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
setCollapsed(!collapsed);
|
||||
}}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
}}
|
||||
>
|
||||
{collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||
</Button>
|
||||
<Menu
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
defaultSelectedKeys={['2']}
|
||||
defaultOpenKeys={['sub2']}
|
||||
inlineCollapsed={collapsed}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,97 @@
|
|||
import Horizon from 'horizon';
|
||||
import { Table } from 'antd';
|
||||
const columns = [
|
||||
{
|
||||
title: 'Full Name',
|
||||
width: 100,
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: 'Lang',
|
||||
width: 100,
|
||||
dataIndex: 'lang',
|
||||
key: 'age',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: 'COL1',
|
||||
dataIndex: 'description',
|
||||
key: '1',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL2',
|
||||
dataIndex: 'description',
|
||||
key: '2',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL3',
|
||||
dataIndex: 'description',
|
||||
key: '3',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL4',
|
||||
dataIndex: 'description',
|
||||
key: '4',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL5',
|
||||
dataIndex: 'description',
|
||||
key: '5',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL6',
|
||||
dataIndex: 'description',
|
||||
key: '6',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL7',
|
||||
dataIndex: 'description',
|
||||
key: '7',
|
||||
width: 220,
|
||||
},
|
||||
{
|
||||
title: 'COL8',
|
||||
dataIndex: 'description',
|
||||
key: '8',
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'operation',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
render: () => <a>action</a>,
|
||||
},
|
||||
];
|
||||
const data = [];
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
data.push({
|
||||
key: i,
|
||||
name: `Horizon ${i}`,
|
||||
lang: 'js',
|
||||
description: `Javascript Framework no. ${i}`,
|
||||
});
|
||||
}
|
||||
|
||||
const App = () => (
|
||||
<div style={{ width: '1200px' }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
scroll={{
|
||||
x: 2200,
|
||||
y: 300,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,29 @@
|
|||
import Horizon from 'horizon';
|
||||
import 'antd/dist/antd.css';
|
||||
import Table from './components/Table';
|
||||
import Menu from './components/Menu';
|
||||
import Menu2 from './components/Menu2';
|
||||
import { Tabs } from 'antd';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const onChange = key => {
|
||||
console.log(key);
|
||||
};
|
||||
const App = () => (
|
||||
<div style={{ padding: '12px' }}>
|
||||
<h1>Horizon ❌ antd</h1>
|
||||
<Tabs defaultActiveKey="Menu" onChange={onChange}>
|
||||
<TabPane tab="Table" key="Table">
|
||||
<Table />
|
||||
</TabPane>
|
||||
<TabPane tab="Menu" key="Menu">
|
||||
<div style={{ display: 'flex' }}>
|
||||
<Menu />
|
||||
<Menu2 />
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
Horizon.render(<App key={1} />, document.getElementById('app'));
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "horizon-antd",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server --config webpack.dev.js --hot --mode development --open"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
"@babel/polyfill": "^7.10.4",
|
||||
"antd": "^4.21.3",
|
||||
"css-loader": "^5.2.2",
|
||||
"style-loader": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.10.4",
|
||||
"@babel/preset-env": "^7.11.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@hot-loader/react-dom": "16.9.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"html-webpack-template": "^6.2.0",
|
||||
"react-hot-loader": "^4.12.20",
|
||||
"webpack": "4.42.0",
|
||||
"webpack-cli": "3.3.11",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack-watch-files-plugin": "^1.2.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const horizon = path.resolve(__dirname, '../../build/horizon');
|
||||
const config = () => {
|
||||
return {
|
||||
entry: ['./index.jsx'],
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'temp'),
|
||||
filename: '[name].[hash].js',
|
||||
},
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts(x)?|js|jsx$/,
|
||||
exclude: /node_modules/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [
|
||||
'style-loader',
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /\.module\.css$/,
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js', '.jsx', 'json'],
|
||||
alias: {
|
||||
horizon: horizon,
|
||||
react: horizon,
|
||||
'react-dom': horizon,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: require('html-webpack-template'),
|
||||
title: 'Horizon Antd',
|
||||
inject: false,
|
||||
appMountId: 'app',
|
||||
filename: 'index.html',
|
||||
}),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = config;
|
|
@ -27,10 +27,12 @@ import {
|
|||
useState,
|
||||
useDebugValue
|
||||
} from './src/renderer/hooks/HookExternal';
|
||||
import { launchUpdateFromVNode as _launchUpdateFromVNode, asyncUpdates } from './src/renderer/TreeBuilder';
|
||||
import { asyncUpdates } from './src/renderer/TreeBuilder';
|
||||
import { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
|
||||
import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler';
|
||||
import { getProcessingVNode as _getProcessingVNode } from './src/renderer/GlobalVar';
|
||||
|
||||
import { createStore, useStore, clearStore } from './src/horizonx/store/StoreHandler';
|
||||
import * as reduxAdapter from './src/horizonx/adapters/redux';
|
||||
|
||||
// act用于测试,作用是:如果fun触发了刷新(包含了异步刷新),可以保证在act后面的代码是在刷新完成后才执行。
|
||||
const act = fun => {
|
||||
|
@ -79,8 +81,10 @@ const Horizon = {
|
|||
findDOMNode,
|
||||
unmountComponentAtNode,
|
||||
act,
|
||||
_launchUpdateFromVNode,
|
||||
_getProcessingVNode,
|
||||
createStore,
|
||||
useStore,
|
||||
clearStore,
|
||||
reduxAdapter,
|
||||
};
|
||||
|
||||
export const version = __VERSION__;
|
||||
|
@ -116,9 +120,11 @@ export {
|
|||
findDOMNode,
|
||||
unmountComponentAtNode,
|
||||
act,
|
||||
// 暂时给HorizonX使用
|
||||
_launchUpdateFromVNode,
|
||||
_getProcessingVNode,
|
||||
// 状态管理器HorizonX接口
|
||||
createStore,
|
||||
useStore,
|
||||
clearStore,
|
||||
reduxAdapter,
|
||||
};
|
||||
|
||||
export default Horizon;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"keywords": [
|
||||
"horizon"
|
||||
],
|
||||
"version": "0.0.7",
|
||||
"version": "0.0.8",
|
||||
"homepage": "",
|
||||
"bugs": "",
|
||||
"main": "index.js",
|
||||
|
|
|
@ -22,9 +22,6 @@ function createRoot(children: any, container: Container, callback?: Callback) {
|
|||
const treeRoot = createTreeRootVNode(container);
|
||||
container._treeRoot = treeRoot;
|
||||
|
||||
// 根节点挂接全量事件
|
||||
listenDelegatedEvents(container as Element);
|
||||
|
||||
// 执行回调
|
||||
if (typeof callback === 'function') {
|
||||
const cb = callback;
|
||||
|
|
|
@ -49,23 +49,17 @@ export function getVNode(dom: Node|Container): VNode | null {
|
|||
|
||||
// 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode
|
||||
export function getNearestVNode(dom: Node): null | VNode {
|
||||
let vNode = dom[INTERNAL_VNODE];
|
||||
if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例
|
||||
return vNode;
|
||||
let domNode: Node | null = dom;
|
||||
// 寻找当前节点及其所有祖先节点是否有标记VNODE
|
||||
while (domNode) {
|
||||
const vNode = domNode[INTERNAL_VNODE];
|
||||
if (vNode) {
|
||||
return vNode;
|
||||
}
|
||||
domNode = domNode.parentNode;
|
||||
}
|
||||
|
||||
// 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过
|
||||
let parentDom = dom.parentNode;
|
||||
let nearVNode = null;
|
||||
while (parentDom) {
|
||||
vNode = parentDom[INTERNAL_VNODE];
|
||||
if (vNode) {
|
||||
nearVNode = vNode;
|
||||
break;
|
||||
}
|
||||
parentDom = parentDom.parentNode;
|
||||
}
|
||||
return nearVNode;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取 vNode 上的属性相关信息
|
||||
|
|
|
@ -26,7 +26,7 @@ import { watchValueChange } from './valueHandler/ValueChangeHandler';
|
|||
import { DomComponent, DomText } from '../renderer/vnode/VNodeTags';
|
||||
import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp';
|
||||
|
||||
export type Props = {
|
||||
export type Props = Record<string, any> & {
|
||||
autoFocus?: boolean;
|
||||
children?: any;
|
||||
dangerouslySetInnerHTML?: any;
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
import {
|
||||
allDelegatedHorizonEvents,
|
||||
} from '../../event/EventCollection';
|
||||
import { allDelegatedHorizonEvents } from '../../event/EventHub';
|
||||
import { updateCommonProp } from './UpdateCommonProp';
|
||||
import { setStyles } from './StyleHandler';
|
||||
import {
|
||||
listenNonDelegatedEvent
|
||||
} from '../../event/EventBinding';
|
||||
import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
|
||||
import { isEventProp } from '../validators/ValidateProps';
|
||||
import { getCurrentRoot } from '../../renderer/TreeBuilder';
|
||||
|
||||
// 初始化DOM属性和更新 DOM 属性
|
||||
export function setDomProps(
|
||||
dom: Element,
|
||||
props: Object,
|
||||
isNativeTag: boolean,
|
||||
isInit: boolean,
|
||||
): void {
|
||||
export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void {
|
||||
const keysOfProps = Object.keys(props);
|
||||
let propName;
|
||||
let propVal;
|
||||
|
@ -27,10 +19,14 @@ export function setDomProps(
|
|||
setStyles(dom, propVal);
|
||||
} else if (isEventProp(propName)) {
|
||||
// 事件监听属性处理
|
||||
const currentRoot = getCurrentRoot();
|
||||
if (!allDelegatedHorizonEvents.has(propName)) {
|
||||
listenNonDelegatedEvent(propName, dom, propVal);
|
||||
} else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) {
|
||||
lazyDelegateOnRoot(currentRoot, propName);
|
||||
}
|
||||
} else if (propName === 'children') { // 只处理纯文本子节点,其他children在VNode树中处理
|
||||
} else if (propName === 'children') {
|
||||
// 只处理纯文本子节点,其他children在VNode树中处理
|
||||
const type = typeof propVal;
|
||||
if (type === 'string' || type === 'number') {
|
||||
dom.textContent = propVal;
|
||||
|
@ -44,10 +40,7 @@ export function setDomProps(
|
|||
}
|
||||
|
||||
// 找出两个 DOM 属性的差别,生成需要更新的属性集合
|
||||
export function compareProps(
|
||||
oldProps: Object,
|
||||
newProps: Object,
|
||||
): Object {
|
||||
export function compareProps(oldProps: Object, newProps: Object): Object {
|
||||
let updatesForStyle = {};
|
||||
const toUpdateProps = {};
|
||||
const keysOfOldProps = Object.keys(oldProps);
|
||||
|
@ -107,7 +100,8 @@ export function compareProps(
|
|||
}
|
||||
|
||||
if (propName === 'style') {
|
||||
if (oldPropValue) { // 之前 style 属性有设置非空值
|
||||
if (oldPropValue) {
|
||||
// 之前 style 属性有设置非空值
|
||||
// 原来有这个 style,但现在没这个 style 了
|
||||
oldStyleProps = Object.keys(oldPropValue);
|
||||
for (let j = 0; j < oldStyleProps.length; j++) {
|
||||
|
@ -125,7 +119,8 @@ export function compareProps(
|
|||
updatesForStyle[styleProp] = newPropValue[styleProp];
|
||||
}
|
||||
}
|
||||
} else { // 之前未设置 style 属性或者设置了空值
|
||||
} else {
|
||||
// 之前未设置 style 属性或者设置了空值
|
||||
if (Object.keys(updatesForStyle).length === 0) {
|
||||
toUpdateProps[propName] = null;
|
||||
}
|
||||
|
@ -144,8 +139,11 @@ export function compareProps(
|
|||
toUpdateProps[propName] = String(newPropValue);
|
||||
}
|
||||
} else if (isEventProp(propName)) {
|
||||
const currentRoot = getCurrentRoot();
|
||||
if (!allDelegatedHorizonEvents.has(propName)) {
|
||||
toUpdateProps[propName] = newPropValue;
|
||||
} else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) {
|
||||
lazyDelegateOnRoot(currentRoot, propName);
|
||||
}
|
||||
} else {
|
||||
toUpdateProps[propName] = newPropValue;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
function isNeedUnitCSS(propName: string) {
|
||||
return !(noUnitCSS.includes(propName)
|
||||
|| propName.startsWith('borderImage')
|
||||
|| propName.startsWith('flex')
|
||||
|| propName.startsWith('gridRow')
|
||||
|| propName.startsWith('gridColumn')
|
||||
|| propName.startsWith('stroke')
|
||||
|| propName.startsWith('box')
|
||||
|| propName.endsWith('Opacity'));
|
||||
function isNeedUnitCSS(styleName: string) {
|
||||
return !(noUnitCSS.includes(styleName)
|
||||
|| styleName.startsWith('borderImage')
|
||||
|| styleName.startsWith('flex')
|
||||
|| styleName.startsWith('gridRow')
|
||||
|| styleName.startsWith('gridColumn')
|
||||
|| styleName.startsWith('stroke')
|
||||
|| styleName.startsWith('box')
|
||||
|| styleName.endsWith('Opacity'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,9 +38,7 @@ export function setStyles(dom, styles) {
|
|||
Object.keys(styles).forEach((name) => {
|
||||
const styleVal = styles[name];
|
||||
|
||||
const validStyleValue = adjustStyleValue(name, styleVal);
|
||||
|
||||
style[name] = validStyleValue;
|
||||
style[name] = adjustStyleValue(name, styleVal);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -56,6 +56,10 @@ export function getDomTag(dom) {
|
|||
return dom.nodeName.toLowerCase();
|
||||
}
|
||||
|
||||
export function isInputElement(dom: Element): dom is HTMLInputElement {
|
||||
return getDomTag(dom) === 'input';
|
||||
}
|
||||
|
||||
const types = ['button', 'input', 'select', 'textarea'];
|
||||
|
||||
// button、input、select、textarea、如果有 autoFocus 属性需要focus
|
||||
|
|
|
@ -6,7 +6,7 @@ const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
|
|||
|
||||
|
||||
// 是内置元素
|
||||
export function isNativeElement(tagName: string, props: Object) {
|
||||
export function isNativeElement(tagName: string, props: Record<string, any>) {
|
||||
return !tagName.includes('-') && props.is === undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp';
|
||||
import {getVNodeProps} from '../DOMInternalKeys';
|
||||
import {IProperty} from '../utils/Interface';
|
||||
import {isInputValueChanged} from './ValueChangeHandler';
|
||||
import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp';
|
||||
import { IProperty } from '../utils/Interface';
|
||||
import { isInputElement } from '../utils/Common';
|
||||
import { getVNodeProps } from '../DOMInternalKeys';
|
||||
import { updateInputValueIfChanged } from './ValueChangeHandler';
|
||||
|
||||
function getInitValue(dom: HTMLInputElement, properties: IProperty) {
|
||||
const {value, defaultValue, checked, defaultChecked} = properties;
|
||||
const { value, defaultValue, checked, defaultChecked } = properties;
|
||||
|
||||
const defaultValueStr = defaultValue != null ? defaultValue : '';
|
||||
const initValue = value != null ? value : defaultValueStr;
|
||||
const initChecked = checked != null ? checked : defaultChecked;
|
||||
|
||||
return {initValue, initChecked};
|
||||
return { initValue, initChecked };
|
||||
}
|
||||
|
||||
export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) {
|
||||
// checked属于必填属性,无法置空
|
||||
// checked属于必填属性,无法置
|
||||
let {checked} = properties;
|
||||
if (checked == null) {
|
||||
checked = getInitValue(dom, properties).initChecked;
|
||||
|
@ -59,30 +60,26 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty)
|
|||
dom.defaultChecked = Boolean(initChecked);
|
||||
}
|
||||
|
||||
export function resetInputValue(dom: HTMLInputElement, properties: IProperty) {
|
||||
const {name, type} = properties;
|
||||
// 如果是 radio,先更新相同 name 的 radio
|
||||
if (type === 'radio' && name != null) {
|
||||
const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`);
|
||||
// 找出同一form内,name相同的Radio,更新它们Handler的Value
|
||||
export function syncRadiosHandler(targetRadio: Element) {
|
||||
if (isInputElement(targetRadio)) {
|
||||
const props = getVNodeProps(targetRadio);
|
||||
if (props) {
|
||||
const { name, type } = props;
|
||||
if (type === 'radio' && name != null) {
|
||||
const radioList = document.querySelectorAll<HTMLInputElement>(`input[type="radio"][name="${name}"]`);
|
||||
for (let i = 0; i < radioList.length; i++) {
|
||||
const radio = radioList[i];
|
||||
if (radio === targetRadio) {
|
||||
continue;
|
||||
}
|
||||
if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let i = 0; i < radioList.length; i++) {
|
||||
const radio = radioList[i];
|
||||
if (radio === dom) {
|
||||
continue;
|
||||
updateInputValueIfChanged(radio);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
if (radio.form !== dom.form) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const nonHorizonRadioProps = getVNodeProps(radio);
|
||||
|
||||
isInputValueChanged(radio);
|
||||
// @ts-ignore
|
||||
updateInputValue(radio, nonHorizonRadioProps);
|
||||
}
|
||||
} else {
|
||||
updateInputValue(dom, properties);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec
|
|||
return {
|
||||
...properties,
|
||||
value: undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) {
|
||||
|
|
|
@ -54,7 +54,7 @@ export function watchValueChange(dom) {
|
|||
}
|
||||
}
|
||||
|
||||
export function isInputValueChanged(dom) {
|
||||
export function updateInputValueIfChanged(dom) {
|
||||
const handler = dom[HANDLER_KEY];
|
||||
if (!handler) {
|
||||
return true;
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
getInputPropsWithoutValue,
|
||||
setInitInputValue,
|
||||
updateInputValue,
|
||||
resetInputValue,
|
||||
} from './InputValueHandler';
|
||||
import {
|
||||
getOptionPropsWithoutValue,
|
||||
|
@ -21,7 +20,6 @@ import {
|
|||
getTextareaPropsWithoutValue,
|
||||
updateTextareaValue,
|
||||
} from './TextareaValueHandler';
|
||||
import {getDomTag} from "../utils/Common";
|
||||
|
||||
// 获取元素除了被代理的值以外的属性
|
||||
function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) {
|
||||
|
@ -73,26 +71,8 @@ function updateValue(type: string, dom: HorizonDom, properties: IProperty) {
|
|||
}
|
||||
}
|
||||
|
||||
function resetValue(dom: HorizonDom, properties: IProperty) {
|
||||
const type = getDomTag(dom);
|
||||
switch (type) {
|
||||
case 'input':
|
||||
resetInputValue(<HTMLInputElement>dom, properties);
|
||||
break;
|
||||
case 'select':
|
||||
updateSelectValue(<HorizonSelect>dom, properties);
|
||||
break;
|
||||
case 'textarea':
|
||||
updateTextareaValue(<HTMLTextAreaElement>dom, properties);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getPropsWithoutValue,
|
||||
setInitValue,
|
||||
updateValue,
|
||||
resetValue,
|
||||
};
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import {getVNodeProps} from '../dom/DOMInternalKeys';
|
||||
import {resetValue} from '../dom/valueHandler';
|
||||
|
||||
let updateList: Array<any> | null = null;
|
||||
|
||||
// 受控组件值重新赋值
|
||||
function updateValue(target: Element) {
|
||||
const props = getVNodeProps(target);
|
||||
if (props) {
|
||||
resetValue(target, props);
|
||||
}
|
||||
}
|
||||
|
||||
// 存储队列中缓存组件
|
||||
export function addValueUpdateList(target: EventTarget): void {
|
||||
if (updateList) {
|
||||
updateList.push(target);
|
||||
} else {
|
||||
updateList = [target];
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否需要重新赋值
|
||||
export function shouldUpdateValue(): boolean {
|
||||
return updateList !== null && updateList.length > 0;
|
||||
}
|
||||
|
||||
// 从缓存队列中对受控组件进行赋值
|
||||
export function updateControlledValue() {
|
||||
if (!updateList) {
|
||||
return;
|
||||
}
|
||||
updateList.forEach(item => {
|
||||
updateValue(item);
|
||||
});
|
||||
updateList = null;
|
||||
}
|
|
@ -1,52 +1,41 @@
|
|||
/**
|
||||
* 事件绑定实现,分为绑定委托事件和非委托事件
|
||||
*/
|
||||
import {allDelegatedNativeEvents} from './EventCollection';
|
||||
import {isDocument} from '../dom/utils/Common';
|
||||
import {
|
||||
getNearestVNode,
|
||||
getNonDelegatedListenerMap,
|
||||
} from '../dom/DOMInternalKeys';
|
||||
import {runDiscreteUpdates} from '../renderer/TreeBuilder';
|
||||
import {isMounted} from '../renderer/vnode/VNodeUtils';
|
||||
import {SuspenseComponent} from '../renderer/vnode/VNodeTags';
|
||||
import {handleEventMain} from './HorizonEventMain';
|
||||
import {decorateNativeEvent} from './customEvents/EventFactory';
|
||||
allDelegatedHorizonEvents,
|
||||
allDelegatedNativeEvents,
|
||||
} from './EventHub';
|
||||
import { isDocument } from '../dom/utils/Common';
|
||||
import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
|
||||
import { runDiscreteUpdates } from '../renderer/TreeBuilder';
|
||||
import { handleEventMain } from './HorizonEventMain';
|
||||
import { decorateNativeEvent } from './EventWrapper';
|
||||
import { VNode } from '../renderer/vnode/VNode';
|
||||
|
||||
const listeningMarker = '_horizonListening' + Math.random().toString(36).slice(4);
|
||||
const listeningMarker =
|
||||
'_horizonListening' +
|
||||
Math.random()
|
||||
.toString(36)
|
||||
.slice(4);
|
||||
|
||||
// 触发委托事件
|
||||
function triggerDelegatedEvent(
|
||||
nativeEvtName: string,
|
||||
isCapture: boolean,
|
||||
targetDom: EventTarget,
|
||||
nativeEvent, // 事件对象event
|
||||
nativeEvent // 事件对象event
|
||||
) {
|
||||
// 执行之前的调度事件
|
||||
runDiscreteUpdates();
|
||||
|
||||
const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement;
|
||||
let targetVNode = getNearestVNode(nativeEventTarget);
|
||||
const targetVNode = getNearestVNode(nativeEventTarget);
|
||||
|
||||
if (targetVNode !== null) {
|
||||
if (isMounted(targetVNode)) {
|
||||
if (targetVNode.tag === SuspenseComponent) {
|
||||
targetVNode = null;
|
||||
}
|
||||
} else {
|
||||
// vNode已销毁
|
||||
targetVNode = null;
|
||||
}
|
||||
}
|
||||
handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom);
|
||||
}
|
||||
|
||||
// 监听委托事件
|
||||
function listenToNativeEvent(
|
||||
nativeEvtName: string,
|
||||
delegatedElement: Element,
|
||||
isCapture: boolean,
|
||||
): void {
|
||||
function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void {
|
||||
let dom: Element | Document = delegatedElement;
|
||||
// document层次可能触发selectionchange事件,为了捕获这类事件,selectionchange事件绑定在document节点上
|
||||
if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) {
|
||||
|
@ -73,6 +62,22 @@ export function listenDelegatedEvents(dom: Element) {
|
|||
});
|
||||
}
|
||||
|
||||
// 事件懒委托,当用户定义事件后,再进行委托到根节点
|
||||
export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
|
||||
currentRoot.delegatedEvents.add(eventName);
|
||||
|
||||
const isCapture = isCaptureEvent(eventName);
|
||||
const nativeEvents = allDelegatedHorizonEvents.get(eventName);
|
||||
|
||||
nativeEvents.forEach(nativeEvent => {
|
||||
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
|
||||
if (!currentRoot.delegatedNativeEvents.has(nativeFullName)) {
|
||||
listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
|
||||
currentRoot.delegatedNativeEvents.add(nativeFullName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 通过horizon事件名获取到native事件名
|
||||
function getNativeEvtName(horizonEventName, capture) {
|
||||
let nativeName;
|
||||
|
@ -104,11 +109,7 @@ function getWrapperListener(horizonEventName, nativeEvtName, targetElement, list
|
|||
}
|
||||
|
||||
// 非委托事件单独监听到各自dom节点
|
||||
export function listenNonDelegatedEvent(
|
||||
horizonEventName: string,
|
||||
domElement: Element,
|
||||
listener,
|
||||
): void {
|
||||
export function listenNonDelegatedEvent(horizonEventName: string, domElement: Element, listener): void {
|
||||
const isCapture = isCaptureEvent(horizonEventName);
|
||||
const nativeEvtName = getNativeEvtName(horizonEventName, isCapture);
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import {horizonEventToNativeMap} from './const';
|
||||
|
||||
// 需要委托的horizon事件和原生事件对应关系
|
||||
export const allDelegatedHorizonEvents = new Map();
|
||||
// 所有委托的原生事件集合
|
||||
export const allDelegatedNativeEvents = new Set();
|
||||
|
||||
horizonEventToNativeMap.forEach((dependencies, horizonEvent) => {
|
||||
allDelegatedHorizonEvents.set(horizonEvent, dependencies);
|
||||
allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies);
|
||||
|
||||
dependencies.forEach(d => {
|
||||
allDelegatedNativeEvents.add(d);
|
||||
});
|
||||
});
|
|
@ -1,3 +1,7 @@
|
|||
// 需要委托的horizon事件和原生事件对应关系
|
||||
export const allDelegatedHorizonEvents = new Map();
|
||||
// 所有委托的原生事件集合
|
||||
export const allDelegatedNativeEvents = new Set();
|
||||
|
||||
// Horizon事件和原生事件对应关系
|
||||
export const horizonEventToNativeMap = new Map([
|
||||
|
@ -28,16 +32,14 @@ export const horizonEventToNativeMap = new Map([
|
|||
['onCompositionStart', ['compositionstart']],
|
||||
['onCompositionUpdate', ['compositionupdate']],
|
||||
['onChange', ['change', 'click', 'focusout', 'input']],
|
||||
['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin',
|
||||
'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']],
|
||||
['onSelect', ['select']],
|
||||
|
||||
['onAnimationEnd', ['animationend']],
|
||||
['onAnimationIteration', ['animationiteration']],
|
||||
['onAnimationStart', ['animationstart']],
|
||||
['onTransitionEnd', ['transitionend']]
|
||||
['onTransitionEnd', ['transitionend']],
|
||||
]);
|
||||
|
||||
export const CommonEventToHorizonMap = {
|
||||
export const NativeEventToHorizonMap = {
|
||||
click: 'click',
|
||||
dblclick: 'doubleClick',
|
||||
contextmenu: 'contextMenu',
|
||||
|
@ -45,6 +47,7 @@ export const CommonEventToHorizonMap = {
|
|||
focusin: 'focus',
|
||||
focusout: 'blur',
|
||||
input: 'input',
|
||||
select: 'select',
|
||||
keydown: 'keyDown',
|
||||
keypress: 'keyPress',
|
||||
keyup: 'keyUp',
|
||||
|
@ -69,11 +72,22 @@ export const CommonEventToHorizonMap = {
|
|||
compositionend: 'compositionEnd',
|
||||
compositionupdate: 'compositionUpdate',
|
||||
};
|
||||
|
||||
export const CHAR_CODE_SPACE = 32;
|
||||
|
||||
|
||||
export const EVENT_TYPE_BUBBLE = 'Bubble';
|
||||
export const EVENT_TYPE_CAPTURE = 'Capture';
|
||||
export const EVENT_TYPE_ALL = 'All';
|
||||
|
||||
horizonEventToNativeMap.forEach((dependencies, horizonEvent) => {
|
||||
allDelegatedHorizonEvents.set(horizonEvent, dependencies);
|
||||
allDelegatedHorizonEvents.set(horizonEvent + 'Capture', dependencies);
|
||||
|
||||
dependencies.forEach(d => {
|
||||
allDelegatedNativeEvents.add(d);
|
||||
});
|
||||
});
|
||||
|
||||
export function transformToHorizonEvent(nativeEvtName: string) {
|
||||
const name = NativeEventToHorizonMap[nativeEvtName];
|
||||
// 例:dragEnd -> onDragEnd
|
||||
return !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`;
|
||||
}
|
|
@ -1,23 +1,75 @@
|
|||
import type { AnyNativeEvent } from './Types';
|
||||
import { AnyNativeEvent, ListenerUnitList } from './Types';
|
||||
import type { VNode } from '../renderer/Types';
|
||||
|
||||
import { isInputElement, setPropertyWritable } from './utils';
|
||||
import { decorateNativeEvent } from './EventWrapper';
|
||||
import { getListenersFromTree } from './ListenerGetter';
|
||||
import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer';
|
||||
import { findRoot } from '../renderer/vnode/VNodeUtils';
|
||||
import { syncRadiosHandler } from '../dom/valueHandler/InputValueHandler';
|
||||
import {
|
||||
CommonEventToHorizonMap,
|
||||
horizonEventToNativeMap,
|
||||
EVENT_TYPE_ALL,
|
||||
EVENT_TYPE_BUBBLE,
|
||||
EVENT_TYPE_CAPTURE,
|
||||
} from './const';
|
||||
import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler';
|
||||
import { getListeners as getSelectionListeners } from './simulatedEvtHandler/SelectionEventHandler';
|
||||
import {
|
||||
setPropertyWritable,
|
||||
} from './utils';
|
||||
import { decorateNativeEvent } from './customEvents/EventFactory';
|
||||
import { getListenersFromTree } from './ListenerGetter';
|
||||
import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater';
|
||||
import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer';
|
||||
import { getExactNode } from '../renderer/vnode/VNodeUtils';
|
||||
import {ListenerUnitList} from './Types';
|
||||
horizonEventToNativeMap,
|
||||
transformToHorizonEvent,
|
||||
} from './EventHub';
|
||||
import { getDomTag } from '../dom/utils/Common';
|
||||
import { updateInputValueIfChanged } from '../dom/valueHandler/ValueChangeHandler';
|
||||
import { getDom } from '../dom/DOMInternalKeys';
|
||||
|
||||
// web规范,鼠标右键key值
|
||||
const RIGHT_MOUSE_BUTTON = 2;
|
||||
|
||||
// 返回是否需要触发change事件标记
|
||||
// | 元素 | 事件 | 需要值变更 |
|
||||
// | --- | --- | --------------- |
|
||||
// | <select/> / <input type="file/> | change | NO |
|
||||
// | <input type="checkbox" /> <input type="radio" /> | click | YES |
|
||||
// | <input type="input /> / <input type="text" /> | input / change | YES |
|
||||
function shouldTriggerChangeEvent(targetDom, evtName) {
|
||||
const { type } = targetDom;
|
||||
const domTag = getDomTag(targetDom);
|
||||
|
||||
if (domTag === 'select' || (domTag === 'input' && type === 'file')) {
|
||||
return evtName === 'change';
|
||||
} else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) {
|
||||
if (evtName === 'click') {
|
||||
return updateInputValueIfChanged(targetDom);
|
||||
}
|
||||
} else if (isInputElement(targetDom)) {
|
||||
if (evtName === 'input' || evtName === 'change') {
|
||||
return updateInputValueIfChanged(targetDom);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 支持input/textarea/select的onChange事件
|
||||
*/
|
||||
function getChangeListeners(
|
||||
nativeEvtName: string,
|
||||
nativeEvt: AnyNativeEvent,
|
||||
vNode: null | VNode,
|
||||
): ListenerUnitList {
|
||||
if (!vNode) {
|
||||
return [];
|
||||
}
|
||||
const targetDom = getDom(vNode);
|
||||
|
||||
// 判断是否需要触发change事件
|
||||
if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) {
|
||||
const event = decorateNativeEvent(
|
||||
'onChange',
|
||||
'change',
|
||||
nativeEvt,
|
||||
);
|
||||
return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// 获取事件触发的普通事件监听方法队列
|
||||
function getCommonListeners(
|
||||
|
@ -27,15 +79,14 @@ function getCommonListeners(
|
|||
target: null | EventTarget,
|
||||
isCapture: boolean,
|
||||
): ListenerUnitList {
|
||||
const name = CommonEventToHorizonMap[nativeEvtName];
|
||||
const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例:dragEnd -> onDragEnd
|
||||
const horizonEvtName = transformToHorizonEvent(nativeEvtName);
|
||||
|
||||
if (!horizonEvtName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 鼠标点击右键
|
||||
if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) {
|
||||
if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === RIGHT_MOUSE_BUTTON) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -71,13 +122,16 @@ function processListeners(listenerList: ListenerUnitList): void {
|
|||
});
|
||||
}
|
||||
|
||||
function getProcessListeners(
|
||||
// 触发可以被执行的horizon事件监听
|
||||
function triggerHorizonEvents(
|
||||
nativeEvtName: string,
|
||||
vNode: VNode | null,
|
||||
isCapture: boolean,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
target,
|
||||
isCapture: boolean
|
||||
): ListenerUnitList {
|
||||
vNode: VNode | null,
|
||||
) {
|
||||
const target = nativeEvent.target || nativeEvent.srcElement;
|
||||
let hasTriggeredChangeEvent = false;
|
||||
|
||||
// 触发普通委托事件
|
||||
let listenerList: ListenerUnitList = getCommonListeners(
|
||||
nativeEvtName,
|
||||
|
@ -88,42 +142,22 @@ function getProcessListeners(
|
|||
);
|
||||
|
||||
// 触发特殊handler委托事件
|
||||
if (!isCapture) {
|
||||
if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) {
|
||||
listenerList = listenerList.concat(getChangeListeners(
|
||||
nativeEvtName,
|
||||
nativeEvent,
|
||||
vNode,
|
||||
target,
|
||||
));
|
||||
}
|
||||
|
||||
if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) {
|
||||
listenerList = listenerList.concat(getSelectionListeners(
|
||||
nativeEvtName,
|
||||
nativeEvent,
|
||||
vNode,
|
||||
target,
|
||||
));
|
||||
if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) {
|
||||
const changeListeners = getChangeListeners(
|
||||
nativeEvtName,
|
||||
nativeEvent,
|
||||
vNode,
|
||||
);
|
||||
if (changeListeners.length) {
|
||||
hasTriggeredChangeEvent = true;
|
||||
listenerList = listenerList.concat(changeListeners);
|
||||
}
|
||||
}
|
||||
return listenerList;
|
||||
}
|
||||
|
||||
// 触发可以被执行的horizon事件监听
|
||||
function triggerHorizonEvents(
|
||||
nativeEvtName: string,
|
||||
isCapture: boolean,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
vNode: VNode | null,
|
||||
): void {
|
||||
const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement;
|
||||
|
||||
// 获取委托事件队列
|
||||
const listenerList = getProcessListeners(nativeEvtName, vNode, nativeEvent, nativeEventTarget, isCapture);
|
||||
|
||||
// 处理触发的事件队列
|
||||
processListeners(listenerList);
|
||||
|
||||
return hasTriggeredChangeEvent;
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,11 +170,11 @@ export function handleEventMain(
|
|||
isCapture: boolean,
|
||||
nativeEvent: AnyNativeEvent,
|
||||
vNode: null | VNode,
|
||||
targetContainer: EventTarget,
|
||||
targetDom: EventTarget,
|
||||
): void {
|
||||
let startVNode = vNode;
|
||||
if (startVNode !== null) {
|
||||
startVNode = getExactNode(startVNode, targetContainer);
|
||||
startVNode = findRoot(startVNode, targetDom);
|
||||
if (!startVNode) {
|
||||
return;
|
||||
}
|
||||
|
@ -154,13 +188,15 @@ export function handleEventMain(
|
|||
|
||||
// 没有事件在执行,经过调度再执行事件
|
||||
isInEventsExecution = true;
|
||||
let hasTriggeredChangeEvent = false;
|
||||
try {
|
||||
asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode));
|
||||
hasTriggeredChangeEvent = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode));
|
||||
} finally {
|
||||
isInEventsExecution = false;
|
||||
if (shouldUpdateValue()) {
|
||||
if (hasTriggeredChangeEvent) {
|
||||
runDiscreteUpdates();
|
||||
updateControlledValue();
|
||||
// 若是Radio,同步同组其他Radio的Handler Value
|
||||
syncRadiosHandler(nativeEvent.target as Element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { VNode } from '../renderer/Types';
|
||||
import { DomComponent } from '../renderer/vnode/VNodeTags';
|
||||
import { EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE } from './const';
|
||||
import { AnyNativeEvent, ListenerUnitList } from './Types';
|
||||
import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub';
|
||||
|
||||
// 从vnode属性中获取事件listener
|
||||
function getListenerFromVNode(vNode: VNode, eventName: string): Function | null {
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import {decorateNativeEvent} from '../customEvents/EventFactory';
|
||||
import {getDom} from '../../dom/DOMInternalKeys';
|
||||
import {isInputValueChanged} from '../../dom/valueHandler/ValueChangeHandler';
|
||||
import {addValueUpdateList} from '../ControlledValueUpdater';
|
||||
import {isInputElement} from '../utils';
|
||||
import {EVENT_TYPE_ALL} from '../const';
|
||||
import {AnyNativeEvent, ListenerUnitList} from '../Types';
|
||||
import {
|
||||
getListenersFromTree,
|
||||
} from '../ListenerGetter';
|
||||
import {VNode} from '../../renderer/Types';
|
||||
import {getDomTag} from '../../dom/utils/Common';
|
||||
|
||||
// 返回是否需要触发change事件标记
|
||||
function shouldTriggerChangeEvent(targetDom, evtName) {
|
||||
const { type } = targetDom;
|
||||
const domTag = getDomTag(targetDom);
|
||||
|
||||
if (domTag === 'select' || (domTag === 'input' && type === 'file')) {
|
||||
return evtName === 'change';
|
||||
} else if (domTag === 'input' && (type === 'checkbox' || type === 'radio')) {
|
||||
if (evtName === 'click') {
|
||||
return isInputValueChanged(targetDom);
|
||||
}
|
||||
} else if (isInputElement(targetDom)) {
|
||||
if (evtName === 'input' || evtName === 'change') {
|
||||
return isInputValueChanged(targetDom);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* 支持input/textarea/select的onChange事件
|
||||
*/
|
||||
export function getListeners(
|
||||
nativeEvtName: string,
|
||||
nativeEvt: AnyNativeEvent,
|
||||
vNode: null | VNode,
|
||||
target: null | EventTarget,
|
||||
): ListenerUnitList {
|
||||
if (!vNode) {
|
||||
return [];
|
||||
}
|
||||
const targetDom = getDom(vNode);
|
||||
|
||||
// 判断是否需要触发change事件
|
||||
if (shouldTriggerChangeEvent(targetDom, nativeEvtName)) {
|
||||
addValueUpdateList(target);
|
||||
const event = decorateNativeEvent(
|
||||
'onChange',
|
||||
'change',
|
||||
nativeEvt,
|
||||
);
|
||||
return getListenersFromTree(vNode, 'onChange', event, EVENT_TYPE_ALL);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import {decorateNativeEvent} from '../customEvents/EventFactory';
|
||||
import {shallowCompare} from '../../renderer/utils/compare';
|
||||
import {getFocusedDom} from '../../dom/utils/Common';
|
||||
import {getDom} from '../../dom/DOMInternalKeys';
|
||||
import {isDocument} from '../../dom/utils/Common';
|
||||
import {isInputElement, setPropertyWritable} from '../utils';
|
||||
import type {AnyNativeEvent} from '../Types';
|
||||
import {getListenersFromTree} from '../ListenerGetter';
|
||||
import type {VNode} from '../../renderer/Types';
|
||||
import {EVENT_TYPE_ALL} from '../const';
|
||||
import {ListenerUnitList} from '../Types';
|
||||
|
||||
const horizonEventName = 'onSelect';
|
||||
|
||||
let currentElement = null;
|
||||
let currentVNode = null;
|
||||
let lastSelection: Selection | null = null;
|
||||
|
||||
function initTargetCache(dom, vNode) {
|
||||
if (isInputElement(dom) || dom.contentEditable === 'true') {
|
||||
currentElement = dom;
|
||||
currentVNode = vNode;
|
||||
lastSelection = null;
|
||||
}
|
||||
}
|
||||
|
||||
function clearTargetCache() {
|
||||
currentElement = null;
|
||||
currentVNode = null;
|
||||
lastSelection = null;
|
||||
}
|
||||
|
||||
// 标记是否在鼠标事件过程中
|
||||
let isInMouseEvent = false;
|
||||
|
||||
// 获取节点所在的document对象
|
||||
function getDocument(eventTarget) {
|
||||
if (eventTarget.window === eventTarget) {
|
||||
return eventTarget.document;
|
||||
}
|
||||
if (isDocument(eventTarget)) {
|
||||
return eventTarget;
|
||||
}
|
||||
return eventTarget.ownerDocument;
|
||||
}
|
||||
|
||||
function getSelectEvent(nativeEvent, target) {
|
||||
const doc = getDocument(target);
|
||||
if (isInMouseEvent || currentElement == null || currentElement !== getFocusedDom(doc)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentSelection = window.getSelection();
|
||||
if (!shallowCompare(lastSelection, currentSelection)) {
|
||||
lastSelection = currentSelection;
|
||||
|
||||
const event = decorateNativeEvent(
|
||||
horizonEventName,
|
||||
'select',
|
||||
nativeEvent,
|
||||
);
|
||||
setPropertyWritable(nativeEvent, 'target');
|
||||
event.target = currentElement;
|
||||
|
||||
return getListenersFromTree(
|
||||
currentVNode,
|
||||
horizonEventName,
|
||||
event,
|
||||
EVENT_TYPE_ALL
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 该插件创建一个onSelect事件
|
||||
* 支持元素: input、textarea、contentEditable元素
|
||||
* 触发场景:用户输入、折叠选择、文本选择
|
||||
*/
|
||||
export function getListeners(
|
||||
nativeEvtName: string,
|
||||
nativeEvt: AnyNativeEvent,
|
||||
vNode: null | VNode,
|
||||
target: null | EventTarget,
|
||||
): ListenerUnitList {
|
||||
const targetNode = vNode ? getDom(vNode) : window;
|
||||
let eventUnitList: ListenerUnitList = [];
|
||||
switch (nativeEvtName) {
|
||||
case 'focusin':
|
||||
initTargetCache(targetNode, vNode);
|
||||
break;
|
||||
case 'focusout':
|
||||
clearTargetCache();
|
||||
break;
|
||||
case 'mousedown':
|
||||
isInMouseEvent = true;
|
||||
break;
|
||||
case 'contextmenu':
|
||||
case 'mouseup':
|
||||
case 'dragend':
|
||||
isInMouseEvent = false;
|
||||
eventUnitList = getSelectEvent(nativeEvt, target);
|
||||
break;
|
||||
case 'selectionchange':
|
||||
case 'keydown':
|
||||
case 'keyup':
|
||||
eventUnitList = getSelectEvent(nativeEvt, target);
|
||||
}
|
||||
|
||||
return eventUnitList;
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
|
||||
export function isInputElement(dom?: HTMLElement): boolean {
|
||||
if (dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement;
|
||||
|
||||
}
|
||||
|
||||
export function setPropertyWritable(obj, propName) {
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
export function isObject(obj) {
|
||||
const type = typeof obj;
|
||||
return obj != null && (type === 'object' || type === 'function');
|
||||
}
|
||||
|
||||
export function isSet(obj) {
|
||||
return obj != null && (Object.prototype.toString.call(obj) === '[object Set]' || obj.constructor === Set);
|
||||
}
|
||||
|
||||
export function isWeakSet(obj) {
|
||||
return obj != null && (Object.prototype.toString.call(obj) === '[object WeakSet]' || obj.constructor === WeakSet);
|
||||
}
|
||||
|
||||
export function isMap(obj) {
|
||||
return obj != null && (Object.prototype.toString.call(obj) === '[object Map]' || obj.constructor === Map);
|
||||
}
|
||||
|
||||
export function isWeakMap(obj) {
|
||||
return obj != null && (Object.prototype.toString.call(obj) === '[object WeakMap]' || obj.constructor === WeakMap);
|
||||
}
|
||||
|
||||
export function isArray(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Array]';
|
||||
}
|
||||
|
||||
export function isCollection(obj) {
|
||||
return isSet(obj) || isWeakSet(obj) || isMap(obj) || isWeakMap(obj);
|
||||
}
|
||||
|
||||
export function isString(obj) {
|
||||
return typeof obj === 'string';
|
||||
}
|
||||
|
||||
export function isValidIntegerKey(key) {
|
||||
return isString(key) && key !== 'NaN' && key[0] !== '-' && String(parseInt(key, 10)) === key;
|
||||
}
|
||||
|
||||
export const noop = () => {};
|
||||
|
||||
export function isSame(x, y) {
|
||||
if (!(typeof Object.is === 'function')) {
|
||||
if (x === y) {
|
||||
// +0 != -0
|
||||
return x !== 0 || 1 / x === 1 / y;
|
||||
} else {
|
||||
// NaN == NaN
|
||||
return x !== x && y !== y;
|
||||
}
|
||||
} else {
|
||||
return Object.is(x, y);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// The two constants must be the same as those in horizon.
|
||||
export const FunctionComponent = 'FunctionComponent';
|
||||
export const ClassComponent = 'ClassComponent';
|
|
@ -0,0 +1,124 @@
|
|||
import { createStore as createStoreX } from '../store/StoreHandler';
|
||||
|
||||
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types';
|
||||
|
||||
export { thunk } from './reduxThunk';
|
||||
|
||||
export { Provider, useSelector, useStore, useDispatch, connect, createSelectorHook, createDispatchHook } from './reduxReact';
|
||||
|
||||
type Reducer = (state: any, action: ReduxAction) => any;
|
||||
|
||||
export function createStore(reducer: Reducer, preloadedState: any, enhancers): ReduxStoreHandler {
|
||||
const store = createStoreX({
|
||||
id: 'defaultStore',
|
||||
state: { stateWrapper: preloadedState },
|
||||
actions: {
|
||||
dispatch: (state: { stateWrapper?: any }, action) => {
|
||||
let result;
|
||||
if (state.stateWrapper !== undefined && state.stateWrapper !== null) {
|
||||
result = reducer(state.stateWrapper, action);
|
||||
} else {
|
||||
result = reducer(undefined, action);
|
||||
}
|
||||
|
||||
if (result === undefined) {
|
||||
return;
|
||||
} // NOTE: reducer should never return undefined, in this case, do not change state
|
||||
state.stateWrapper = result;
|
||||
},
|
||||
},
|
||||
options: {
|
||||
suppressHooks: true,
|
||||
},
|
||||
})();
|
||||
|
||||
const result = {
|
||||
reducer,
|
||||
getState: function() {
|
||||
return store.$state.stateWrapper;
|
||||
},
|
||||
subscribe: listener => {
|
||||
store.$subscribe(listener);
|
||||
|
||||
return () => {
|
||||
store.$unsubscribe(listener);
|
||||
};
|
||||
},
|
||||
replaceReducer: newReducer => {
|
||||
reducer = newReducer;
|
||||
},
|
||||
_horizonXstore: store,
|
||||
dispatch: store.$actions.dispatch,
|
||||
};
|
||||
|
||||
enhancers && enhancers(result);
|
||||
|
||||
result.dispatch({ type: 'HorizonX' });
|
||||
|
||||
store.reduxHandler = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function combineReducers(reducers: { [key: string]: Reducer }): Reducer {
|
||||
return (state = {}, action) => {
|
||||
const newState = {};
|
||||
Object.entries(reducers).forEach(([key, reducer]) => {
|
||||
newState[key] = reducer(state[key], action);
|
||||
});
|
||||
return newState;
|
||||
};
|
||||
}
|
||||
|
||||
export function applyMiddleware(...middlewares: ReduxMiddleware[]): (store: ReduxStoreHandler) => void {
|
||||
return store => {
|
||||
return applyMiddlewares(store, middlewares);
|
||||
};
|
||||
}
|
||||
|
||||
function applyMiddlewares(store: ReduxStoreHandler, middlewares: ReduxMiddleware[]): void {
|
||||
middlewares = middlewares.slice();
|
||||
middlewares.reverse();
|
||||
let dispatch = store.dispatch;
|
||||
middlewares.forEach(middleware => {
|
||||
dispatch = middleware(store)(dispatch);
|
||||
});
|
||||
store.dispatch = dispatch;
|
||||
}
|
||||
|
||||
type ActionCreator = (...params: any[]) => ReduxAction;
|
||||
type ActionCreators = { [key: string]: ActionCreator };
|
||||
export type BoundActionCreator = (...params: any[]) => void;
|
||||
type BoundActionCreators = { [key: string]: BoundActionCreator };
|
||||
type Dispatch = (action) => any;
|
||||
|
||||
export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dispatch): BoundActionCreators {
|
||||
const boundActionCreators = {};
|
||||
Object.entries(actionCreators).forEach(([key, value]) => {
|
||||
boundActionCreators[key] = (...args) => {
|
||||
dispatch(value(...args));
|
||||
};
|
||||
});
|
||||
|
||||
return boundActionCreators;
|
||||
}
|
||||
|
||||
export function compose(middlewares: ReduxMiddleware[]) {
|
||||
return (store: ReduxStoreHandler, extraArgument: any) => {
|
||||
let val;
|
||||
middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {
|
||||
if (!index) {
|
||||
val = middleware(store, extraArgument);
|
||||
return;
|
||||
}
|
||||
val = middleware(val);
|
||||
});
|
||||
return val;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// HorizonX batches updates by default, this function is only for backwards compatibility
|
||||
export function batch(fn: () => void) {
|
||||
fn();
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// @ts-ignore
|
||||
import { useState, useContext, useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||
import { createContext } from '../../renderer/components/context/CreateContext';
|
||||
import { createElement } from '../../external/JSXElement';
|
||||
import { BoundActionCreator } from './redux';
|
||||
import { ReduxAction, ReduxStoreHandler } from '../types';
|
||||
|
||||
const DefaultContext = createContext();
|
||||
type Context = typeof DefaultContext;
|
||||
|
||||
export function Provider({
|
||||
store,
|
||||
context = DefaultContext,
|
||||
children,
|
||||
}: {
|
||||
store: ReduxStoreHandler;
|
||||
context: Context;
|
||||
children?: any[];
|
||||
}) {
|
||||
const Context = context; // NOTE: bind redux API to horizon API requires this renaming;
|
||||
return createElement(Context.Provider, { value: store }, children);
|
||||
}
|
||||
|
||||
export function createStoreHook(context: Context) {
|
||||
return () => {
|
||||
return useContext(context);
|
||||
};
|
||||
}
|
||||
|
||||
export function createSelectorHook(context: Context): (selector: (any) => any) => any {
|
||||
const store = createStoreHook(context)();
|
||||
return function(selector = state => state) {
|
||||
const [b, fr] = useState(false);
|
||||
|
||||
const listener = () => {
|
||||
fr(!b);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(listener);
|
||||
|
||||
return () => {
|
||||
unsubscribe(listener);
|
||||
};
|
||||
});
|
||||
|
||||
return selector(store.getState());
|
||||
};
|
||||
}
|
||||
|
||||
export function createDispatchHook(context: Context): BoundActionCreator {
|
||||
const store = createStoreHook(context)();
|
||||
return function() {
|
||||
return action => {
|
||||
this.dispatch(action);
|
||||
};
|
||||
}.bind(store);
|
||||
}
|
||||
|
||||
export const useSelector = selector => {
|
||||
return createSelectorHook(DefaultContext)(selector);
|
||||
};
|
||||
|
||||
export const useDispatch = () => {
|
||||
return createDispatchHook(DefaultContext)();
|
||||
};
|
||||
|
||||
export const useStore = () => {
|
||||
return createStoreHook(DefaultContext)();
|
||||
};
|
||||
|
||||
// function shallowCompare(a,b){
|
||||
// return Object.keys(a).length === Object.keys(b).length &&
|
||||
// Object.keys(a).every(key => a[key] === b[key]);
|
||||
// }
|
||||
|
||||
//TODO: implement options
|
||||
// context?: Object,
|
||||
// areStatesEqual?: Function, :)
|
||||
// areOwnPropsEqual?: Function,
|
||||
// areStatePropsEqual?: Function,
|
||||
// areMergedPropsEqual?: Function,
|
||||
// forwardRef?: boolean,
|
||||
// const defaultOptions = {
|
||||
// areStatesEqual: shallowCompare,
|
||||
// areOwnPropsEqual: shallowCompare,
|
||||
// areStatePropsEqual: shallowCompare,
|
||||
// areMergedPropsEqual: shallowCompare
|
||||
// };
|
||||
|
||||
export function connect(
|
||||
mapStateToProps?: (state: any, ownProps: { [key: string]: any }) => Object,
|
||||
mapDispatchToProps?:
|
||||
| { [key: string]: (...args: any[]) => ReduxAction }
|
||||
| ((dispatch: (action: ReduxAction) => any, ownProps?: Object) => Object),
|
||||
mergeProps?: (stateProps: Object, dispatchProps: Object, ownProps: Object) => Object,
|
||||
options?: {
|
||||
areStatesEqual?: (oldState: any, newState: any) => boolean;
|
||||
context?: any; // TODO: type this
|
||||
}
|
||||
) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
|
||||
return Component => {
|
||||
const useStore = createStoreHook(options.context || DefaultContext);
|
||||
|
||||
function Wrapper(props) {
|
||||
const [f, forceReload] = useState(true);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = store.subscribe(() => forceReload(!f));
|
||||
() => {
|
||||
unsubscribe(() => forceReload(!f));
|
||||
};
|
||||
});
|
||||
|
||||
const previous = useRef({
|
||||
state: {},
|
||||
});
|
||||
|
||||
let mappedState;
|
||||
if (options.areStatesEqual) {
|
||||
if (options.areStatesEqual(previous.current.state, store.getState())) {
|
||||
mappedState = previous.current.mappedState;
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
} else {
|
||||
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
|
||||
previous.current.mappedState = mappedState;
|
||||
}
|
||||
let mappedDispatch: { dispatch?: (action) => void } = {};
|
||||
if (mapDispatchToProps) {
|
||||
if (typeof mapDispatchToProps === 'object') {
|
||||
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
|
||||
mappedDispatch[key] = (...args) => {
|
||||
store.dispatch(value(...args));
|
||||
};
|
||||
});
|
||||
} else {
|
||||
mappedDispatch = mapDispatchToProps(store.dispatch, props);
|
||||
}
|
||||
} else {
|
||||
mappedDispatch.dispatch = store.dispatch;
|
||||
}
|
||||
const mergedProps = (
|
||||
mergeProps ||
|
||||
((state, dispatch, originalProps) => {
|
||||
return { ...state, ...dispatch, ...originalProps };
|
||||
})
|
||||
)(mappedState, mappedDispatch, props);
|
||||
|
||||
previous.current.state = store.getState();
|
||||
|
||||
const node = createElement(Component, mergedProps);
|
||||
return node;
|
||||
}
|
||||
|
||||
return Wrapper;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { ReduxStoreHandler, ReduxAction, ReduxMiddleware } from '../types';
|
||||
|
||||
function createThunkMiddleware(extraArgument?: any): ReduxMiddleware {
|
||||
return (store: ReduxStoreHandler) => (next: (action: ReduxAction) => any) => (
|
||||
action:
|
||||
| ReduxAction
|
||||
| ((dispatch: (action: ReduxAction) => void, store: ReduxStoreHandler, extraArgument?: any) => any)
|
||||
) => {
|
||||
// This gets called for every action you dispatch.
|
||||
// If it's a function, call it.
|
||||
if (typeof action === 'function') {
|
||||
return action(store.dispatch, store.getState.bind(store), extraArgument);
|
||||
}
|
||||
|
||||
// Otherwise, just continue processing this action as usual
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
export const thunk = createThunkMiddleware();
|
||||
// @ts-ignore
|
||||
thunk.withExtraArgument = createThunkMiddleware;
|
|
@ -0,0 +1,45 @@
|
|||
// TODO: implement vNode type
|
||||
|
||||
import {IObserver} from '../types';
|
||||
|
||||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
*
|
||||
*/
|
||||
export class HooklessObserver implements IObserver {
|
||||
|
||||
listeners:(() => void)[] = [];
|
||||
|
||||
useProp(key: string): void {
|
||||
}
|
||||
|
||||
addListener(listener: () => void) {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
removeListener(listener: () => void) {
|
||||
this.listeners = this.listeners.filter(item => item != listener);
|
||||
}
|
||||
|
||||
setProp(key: string): void {
|
||||
this.triggerChangeListeners();
|
||||
}
|
||||
|
||||
triggerChangeListeners(): void {
|
||||
this.listeners.forEach(listener => {
|
||||
if (!listener) {
|
||||
return;
|
||||
}
|
||||
listener();
|
||||
});
|
||||
}
|
||||
|
||||
triggerUpdate(vNode): void {
|
||||
}
|
||||
|
||||
allChange(): void {
|
||||
}
|
||||
|
||||
clearByVNode(vNode): void {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* 一个对象(对象、数组、集合)对应一个Observer
|
||||
*/
|
||||
|
||||
//@ts-ignore
|
||||
import { launchUpdateFromVNode } from '../../renderer/TreeBuilder';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { VNode } from '../../renderer/vnode/VNode';
|
||||
import { IObserver } from '../types';
|
||||
|
||||
export class Observer implements IObserver {
|
||||
|
||||
vNodeKeys = new WeakMap();
|
||||
|
||||
keyVNodes = new Map();
|
||||
|
||||
listeners:(()=>void)[] = [];
|
||||
|
||||
useProp(key: string): void {
|
||||
const processingVNode = getProcessingVNode();
|
||||
if (processingVNode === null || !processingVNode.observers) {
|
||||
return;
|
||||
}
|
||||
|
||||
// vNode -> Observers
|
||||
processingVNode.observers.add(this);
|
||||
|
||||
// key -> vNodes
|
||||
let vNodes = this.keyVNodes.get(key);
|
||||
if (!vNodes) {
|
||||
vNodes = new Set();
|
||||
this.keyVNodes.set(key, vNodes);
|
||||
}
|
||||
vNodes.add(processingVNode);
|
||||
|
||||
// vNode -> keys
|
||||
let keys = this.vNodeKeys.get(processingVNode);
|
||||
if (!keys) {
|
||||
keys = new Set();
|
||||
this.vNodeKeys.set(processingVNode, keys);
|
||||
}
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
addListener(listener: () => void): void {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
removeListener(listener: () => void): void {
|
||||
this.listeners = this.listeners.filter(item => item != listener);
|
||||
}
|
||||
|
||||
setProp(key: string): void {
|
||||
const vNodes = this.keyVNodes.get(key);
|
||||
vNodes?.forEach((vNode: VNode) => {
|
||||
if (vNode.isStoreChange) {
|
||||
// update already triggered
|
||||
return;
|
||||
}
|
||||
vNode.isStoreChange = true;
|
||||
|
||||
// 触发vNode更新
|
||||
this.triggerUpdate(vNode);
|
||||
});
|
||||
this.triggerChangeListeners();
|
||||
}
|
||||
|
||||
triggerChangeListeners(): void {
|
||||
this.listeners.forEach(listener => listener());
|
||||
}
|
||||
|
||||
triggerUpdate(vNode: VNode): void {
|
||||
if (!vNode) {
|
||||
return;
|
||||
}
|
||||
launchUpdateFromVNode(vNode);
|
||||
}
|
||||
|
||||
allChange(): void {
|
||||
let keyIt = this.keyVNodes.keys();
|
||||
let keyItem = keyIt.next();
|
||||
while (!keyItem.done) {
|
||||
this.setProp(keyItem.value);
|
||||
keyItem = keyIt.next();
|
||||
}
|
||||
}
|
||||
|
||||
clearByVNode(vNode: Vnode): void {
|
||||
const keys = this.vNodeKeys.get(vNode);
|
||||
if (keys) {
|
||||
keys.forEach((key: any) => {
|
||||
const vNodes = this.keyVNodes.get(key);
|
||||
vNodes.delete(vNode);
|
||||
if (vNodes.size === 0) {
|
||||
this.keyVNodes.delete(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.vNodeKeys.delete(vNode);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import {createObjectProxy} from './handlers/ObjectProxyHandler';
|
||||
import {Observer} from './Observer';
|
||||
import {HooklessObserver} from './HooklessObserver';
|
||||
import {isArray, isCollection, isObject} from '../CommonUtils';
|
||||
import {createArrayProxy} from './handlers/ArrayProxyHandler';
|
||||
import {createCollectionProxy} from './handlers/CollectionProxyHandler';
|
||||
import { IObserver } from '../types';
|
||||
|
||||
const OBSERVER_KEY = Symbol('_horizonObserver');
|
||||
|
||||
const proxyMap = new WeakMap();
|
||||
|
||||
export const hookObserverMap = new WeakMap();
|
||||
|
||||
export function createProxy(rawObj: any, hookObserver = true): any {
|
||||
// 不是对象(是原始数据类型)不用代理
|
||||
if (!isObject(rawObj)) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
const existProxy = proxyMap.get(rawObj);
|
||||
if (existProxy) {
|
||||
return existProxy;
|
||||
}
|
||||
|
||||
// Observer不需要代理
|
||||
if (rawObj instanceof Observer) {
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// 创建Observer
|
||||
let observer:IObserver = getObserver(rawObj);
|
||||
if (!observer) {
|
||||
observer = hookObserver ? new Observer() : new HooklessObserver();
|
||||
rawObj[OBSERVER_KEY] = observer;
|
||||
}
|
||||
|
||||
hookObserverMap.set(rawObj, hookObserver);
|
||||
|
||||
// 创建Proxy
|
||||
let proxyObj;
|
||||
if (isArray(rawObj)) {
|
||||
// 数组
|
||||
proxyObj = createArrayProxy(rawObj as []);
|
||||
} else if (isCollection(rawObj)) {
|
||||
// 集合
|
||||
proxyObj = createCollectionProxy(rawObj);
|
||||
} else {
|
||||
// 原生对象 或 函数
|
||||
proxyObj = createObjectProxy(rawObj);
|
||||
}
|
||||
|
||||
proxyMap.set(rawObj, proxyObj);
|
||||
proxyMap.set(proxyObj, proxyObj);
|
||||
|
||||
return proxyObj;
|
||||
}
|
||||
|
||||
export function getObserver(rawObj: any): Observer {
|
||||
return rawObj[OBSERVER_KEY];
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { getObserver } from '../ProxyHandler';
|
||||
import { isSame, isValidIntegerKey } from '../../CommonUtils';
|
||||
import { get as objectGet } from './ObjectProxyHandler';
|
||||
|
||||
export function createArrayProxy(rawObj: any[]): any[] {
|
||||
const handle = {
|
||||
get,
|
||||
set,
|
||||
};
|
||||
|
||||
return new Proxy(rawObj, handle);
|
||||
}
|
||||
|
||||
function get(rawObj: any[], key: string, receiver: any) {
|
||||
if (isValidIntegerKey(key) || key === 'length') {
|
||||
return objectGet(rawObj, key, receiver);
|
||||
}
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
function set(rawObj: any[], key: string, value: any, receiver: any) {
|
||||
const oldValue = rawObj[key];
|
||||
const oldLength = rawObj.length;
|
||||
const newValue = value;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
const newLength = rawObj.length;
|
||||
const tracker = getObserver(rawObj);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
tracker.setProp(key);
|
||||
}
|
||||
|
||||
if (oldLength !== newLength) {
|
||||
tracker.setProp('length');
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
import { isMap, isWeakMap, isSame } from '../../CommonUtils';
|
||||
|
||||
const COLLECTION_CHANGE = '_collectionChange';
|
||||
const handler = {
|
||||
get,
|
||||
set,
|
||||
add,
|
||||
delete: deleteFun,
|
||||
clear,
|
||||
has,
|
||||
entries,
|
||||
forEach,
|
||||
keys,
|
||||
values,
|
||||
[Symbol.iterator]: forOf,
|
||||
};
|
||||
|
||||
export function createCollectionProxy(rawObj: Object, hookObserver = true): Object {
|
||||
const boundHandler = {};
|
||||
Object.entries(handler).forEach(([id, val]) => {
|
||||
boundHandler[id] = (...args: any[]) => {
|
||||
return (val as any)(...args, hookObserver);
|
||||
};
|
||||
});
|
||||
return new Proxy(rawObj, { ...boundHandler });
|
||||
}
|
||||
|
||||
function get(rawObj: { size: number }, key: any, receiver: any): any {
|
||||
if (key === 'size') {
|
||||
return size(rawObj);
|
||||
} else if (key === 'get') {
|
||||
return getFun.bind(null, rawObj);
|
||||
} else if (Object.prototype.hasOwnProperty.call(handler, key)) {
|
||||
const value = Reflect.get(handler, key, receiver);
|
||||
return value.bind(null, rawObj);
|
||||
}
|
||||
|
||||
return Reflect.get(rawObj, key, receiver);
|
||||
}
|
||||
|
||||
function getFun(rawObj: { get: (key: any) => any }, key: any) {
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.useProp(key);
|
||||
|
||||
const value = rawObj.get(key);
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
// Map的set方法
|
||||
function set(
|
||||
rawObj: { get: (key: any) => any; set: (key: any, value: any) => any; has: (key: any) => boolean },
|
||||
key: any,
|
||||
value: any
|
||||
) {
|
||||
const oldValue = rawObj.get(key);
|
||||
const newValue = value;
|
||||
rawObj.set(key, newValue);
|
||||
const valChange = !isSame(newValue, oldValue);
|
||||
const tracker = getObserver(rawObj);
|
||||
|
||||
if (valChange || !rawObj.has(key)) {
|
||||
tracker.setProp(COLLECTION_CHANGE);
|
||||
}
|
||||
|
||||
if (valChange) {
|
||||
tracker.setProp(key);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
// Set的add方法
|
||||
function add(rawObj: { add: (any) => void; set: (string, any) => any; has: (any) => boolean }, value: any): Object {
|
||||
if (!rawObj.has(value)) {
|
||||
rawObj.add(value);
|
||||
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.setProp(value);
|
||||
tracker.setProp(COLLECTION_CHANGE);
|
||||
}
|
||||
|
||||
return rawObj;
|
||||
}
|
||||
|
||||
function has(rawObj: { has: (string) => boolean }, key: any): boolean {
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.useProp(key);
|
||||
|
||||
return rawObj.has(key);
|
||||
}
|
||||
|
||||
function clear(rawObj: { size: number; clear: () => void }) {
|
||||
const oldSize = rawObj.size;
|
||||
rawObj.clear();
|
||||
|
||||
if (oldSize > 0) {
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.allChange();
|
||||
}
|
||||
}
|
||||
|
||||
function deleteFun(rawObj: { has: (key: any) => boolean; delete: (key: any) => void }, key: any) {
|
||||
if (rawObj.has(key)) {
|
||||
rawObj.delete(key);
|
||||
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.setProp(key);
|
||||
tracker.setProp(COLLECTION_CHANGE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function size(rawObj: { size: number }) {
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.useProp(COLLECTION_CHANGE);
|
||||
return rawObj.size;
|
||||
}
|
||||
|
||||
function keys(rawObj: { keys: () => { next: () => { value: any; done: boolean } } }) {
|
||||
return wrapIterator(rawObj, rawObj.keys());
|
||||
}
|
||||
|
||||
function values(rawObj: { values: () => { next: () => { value: any; done: boolean } } }) {
|
||||
return wrapIterator(rawObj, rawObj.values());
|
||||
}
|
||||
|
||||
function entries(rawObj: { entries: () => { next: () => { value: any; done: boolean } } }) {
|
||||
return wrapIterator(rawObj, rawObj.entries(), true);
|
||||
}
|
||||
|
||||
function forOf(rawObj: {
|
||||
entries: () => { next: () => { value: any; done: boolean } };
|
||||
values: () => { next: () => { value: any; done: boolean } };
|
||||
}) {
|
||||
const isMapType = isMap(rawObj) || isWeakMap(rawObj);
|
||||
const iterator = isMapType ? rawObj.entries() : rawObj.values();
|
||||
return wrapIterator(rawObj, iterator, isMapType);
|
||||
}
|
||||
|
||||
function forEach(
|
||||
rawObj: { forEach: (callback: (value: any, key: any) => void) => void },
|
||||
callback: (valProxy: any, keyProxy: any, rawObj: any) => void
|
||||
) {
|
||||
const tracker = getObserver(rawObj);
|
||||
tracker.useProp(COLLECTION_CHANGE);
|
||||
rawObj.forEach((value, key) => {
|
||||
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||
const keyProxy = createProxy(key, hookObserverMap.get(rawObj));
|
||||
// 最后一个参数要返回代理对象
|
||||
return callback(valProxy, keyProxy, rawObj);
|
||||
});
|
||||
}
|
||||
|
||||
function wrapIterator(rawObj: Object, rawIt: { next: () => { value: any; done: boolean } }, isPair = false) {
|
||||
const tracker = getObserver(rawObj);
|
||||
const hookObserver = hookObserverMap.get(rawObj);
|
||||
tracker.useProp(COLLECTION_CHANGE);
|
||||
|
||||
return {
|
||||
next() {
|
||||
const { value, done } = rawIt.next();
|
||||
if (done) {
|
||||
return { value: createProxy(value, hookObserver), done };
|
||||
}
|
||||
|
||||
tracker.useProp(COLLECTION_CHANGE);
|
||||
|
||||
let newVal;
|
||||
if (isPair) {
|
||||
newVal = [createProxy(value[0], hookObserver), createProxy(value[1], hookObserver)];
|
||||
} else {
|
||||
newVal = createProxy(value, hookObserver);
|
||||
}
|
||||
|
||||
return { value: newVal, done };
|
||||
},
|
||||
[Symbol.iterator]() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { isSame } from '../../CommonUtils';
|
||||
import { createProxy, getObserver, hookObserverMap } from '../ProxyHandler';
|
||||
|
||||
export function createObjectProxy<T extends object>(rawObj: T): ProxyHandler<T> {
|
||||
const proxy = new Proxy(rawObj, {
|
||||
get,
|
||||
set,
|
||||
});
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export function get(rawObj: object, key: string, receiver: any): any {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (key === 'addListener') {
|
||||
return observer.addListener.bind(observer);
|
||||
}
|
||||
|
||||
if (key === 'removeListener') {
|
||||
return observer.removeListener.bind(observer);
|
||||
}
|
||||
|
||||
observer.useProp(key);
|
||||
|
||||
const value = Reflect.get(rawObj, key, receiver);
|
||||
|
||||
// 对于value也需要进一步代理
|
||||
const valProxy = createProxy(value, hookObserverMap.get(rawObj));
|
||||
|
||||
return valProxy;
|
||||
}
|
||||
|
||||
export function set(rawObj: object, key: string, value: any, receiver: any): boolean {
|
||||
const observer = getObserver(rawObj);
|
||||
|
||||
if (value && key == 'removeListener') {
|
||||
observer.removeListener(value);
|
||||
}
|
||||
const oldValue = rawObj[key];
|
||||
const newValue = value;
|
||||
|
||||
const ret = Reflect.set(rawObj, key, newValue, receiver);
|
||||
|
||||
if (!isSame(newValue, oldValue)) {
|
||||
observer.setProp(key);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { isObject } from '../CommonUtils';
|
||||
|
||||
export function readonlyProxy<T extends object>(target: T): ProxyHandler<T> {
|
||||
return new Proxy(target, {
|
||||
get(target, property, receiver) {
|
||||
const result = Reflect.get(target, property, receiver);
|
||||
try {
|
||||
if (isObject(result)) {
|
||||
return readonlyProxy(result);
|
||||
}
|
||||
} catch {}
|
||||
return result;
|
||||
},
|
||||
|
||||
set() {
|
||||
throw Error('Trying to change readonly variable');
|
||||
},
|
||||
|
||||
deleteProperty() {
|
||||
throw Error('Trying to change readonly variable');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default readonlyProxy;
|
|
@ -0,0 +1,219 @@
|
|||
//@ts-ignore
|
||||
import { useEffect, useRef } from '../../renderer/hooks/HookExternal';
|
||||
import { getProcessingVNode } from '../../renderer/GlobalVar';
|
||||
import { createProxy } from '../proxy/ProxyHandler';
|
||||
import readonlyProxy from '../proxy/readonlyProxy';
|
||||
import { StoreHandler, StoreConfig, UserActions, UserComputedValues, StoreActions, ComputedValues, ActionFunction, Action, QueuedStoreActions } from '../types';
|
||||
import { Observer } from '../proxy/Observer';
|
||||
import { FunctionComponent, ClassComponent } from '../Constants';
|
||||
|
||||
const storeMap = new Map<string,StoreHandler<any,any,any>>();
|
||||
|
||||
function isPromise(obj: any): boolean {
|
||||
return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function';
|
||||
}
|
||||
|
||||
type PlannedAction<S extends object,F extends ActionFunction<S>>={
|
||||
action:string,
|
||||
payload: any[],
|
||||
resolve: ReturnType<F>
|
||||
}
|
||||
|
||||
export function createStore<S extends object,A extends UserActions<S>,C extends UserComputedValues<S>>(config: StoreConfig<S,A,C>): () => StoreHandler<S,A,C> {
|
||||
//create a local shalow copy to ensure consistency (if user would change the config object after store creation)
|
||||
config = {
|
||||
id:config.id,
|
||||
options: config.options,
|
||||
state: config.state,
|
||||
actions: config.actions ? {...config.actions}:undefined,
|
||||
computed: config.computed ? {...config.computed}:undefined
|
||||
}
|
||||
|
||||
// 校验
|
||||
if (Object.prototype.toString.call(config) !== '[object Object]') {
|
||||
throw new Error('store obj must be pure object');
|
||||
}
|
||||
|
||||
const proxyObj = createProxy(config.state, !config.options?.suppressHooks);
|
||||
|
||||
proxyObj.$pending = false;
|
||||
|
||||
const $subscribe = (listener) => {
|
||||
proxyObj.addListener(listener);
|
||||
};
|
||||
|
||||
const $unsubscribe = (listener) => {
|
||||
proxyObj.removeListener(listener);
|
||||
};
|
||||
|
||||
const plannedActions:PlannedAction<S,ActionFunction<S>>[] = [];
|
||||
const $actions:Partial<StoreActions<S,A>>={}
|
||||
const $queue:Partial<StoreActions<S,A>> = {};
|
||||
const $computed:Partial<ComputedValues<S,C>>={}
|
||||
const handler = {
|
||||
$subscribe,
|
||||
$unsubscribe,
|
||||
$actions:$actions as StoreActions<S,A>,
|
||||
$state:proxyObj,
|
||||
$computed: $computed as ComputedValues<S,C>,
|
||||
$config:config,
|
||||
$queue: $queue as QueuedStoreActions<S,A>,
|
||||
} as StoreHandler<S,A,C>;
|
||||
|
||||
function tryNextAction() {
|
||||
if (!plannedActions.length) {
|
||||
proxyObj.$pending = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextAction = plannedActions.shift()!;
|
||||
const result = config.actions ? config.actions[nextAction.action].bind(self, proxyObj)(...nextAction.payload) : undefined;
|
||||
|
||||
if (isPromise(result)) {
|
||||
result.then(value => {
|
||||
nextAction.resolve(value);
|
||||
tryNextAction();
|
||||
});
|
||||
} else {
|
||||
nextAction.resolve(result);
|
||||
tryNextAction();
|
||||
}
|
||||
}
|
||||
|
||||
// 包装actions
|
||||
if(config.actions){
|
||||
Object.keys(config.actions).forEach(action => {
|
||||
($queue as any)[action] = (...payload) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!proxyObj.$pending) {
|
||||
proxyObj.$pending = true;
|
||||
const result = config.actions![action].bind(self, proxyObj)(...payload);
|
||||
|
||||
if (isPromise(result)) {
|
||||
result.then((value) => {
|
||||
resolve(value);
|
||||
tryNextAction();
|
||||
});
|
||||
} else {
|
||||
resolve(result);
|
||||
tryNextAction();
|
||||
}
|
||||
} else {
|
||||
plannedActions.push({
|
||||
action,
|
||||
payload,
|
||||
resolve
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
($actions as any)[action] = function Wrapped(...payload) {
|
||||
return config.actions![action].bind(self, proxyObj)(...payload);
|
||||
};
|
||||
|
||||
// direct store access
|
||||
Object.defineProperty(handler, action, {
|
||||
writable: false,
|
||||
value: $actions[action]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.computed) {
|
||||
Object.keys(config.computed).forEach((key) => {
|
||||
($computed as any)[key] = config.computed![key].bind(handler, readonlyProxy(proxyObj));
|
||||
|
||||
// direct store access
|
||||
Object.defineProperty(handler, key, {
|
||||
get: $computed[key] as ()=>any
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// direct state access
|
||||
if(config.state){
|
||||
Object.keys(config.state).forEach(key => {
|
||||
Object.defineProperty(handler, key, {
|
||||
get: () => proxyObj[key]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (config.id) {
|
||||
storeMap.set(config.id, handler);
|
||||
}
|
||||
|
||||
return createStoreHook(handler);
|
||||
}
|
||||
|
||||
function clearVNodeObservers(vNode) {
|
||||
vNode.observers.forEach(observer => {
|
||||
observer.clearByVNode(vNode);
|
||||
});
|
||||
|
||||
vNode.observers.clear();
|
||||
}
|
||||
|
||||
function hookStore() {
|
||||
const processingVNode = getProcessingVNode();
|
||||
|
||||
// did not execute in a component
|
||||
if (!processingVNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (processingVNode.observers) {
|
||||
// 清除上一次缓存的Observer依赖
|
||||
clearVNodeObservers(processingVNode);
|
||||
} else {
|
||||
processingVNode.observers = new Set<Observer>();
|
||||
}
|
||||
|
||||
if (processingVNode.tag === FunctionComponent) {
|
||||
// from FunctionComponent
|
||||
const vNodeRef = useRef(null);
|
||||
vNodeRef.current = processingVNode;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearVNodeObservers(vNodeRef.current);
|
||||
vNodeRef.current.observers = null;
|
||||
};
|
||||
}, []);
|
||||
} else if (processingVNode.tag === ClassComponent) {
|
||||
// from ClassComponent
|
||||
if (!processingVNode.classComponentWillUnmount) {
|
||||
processingVNode.classComponentWillUnmount = function(vNode) {
|
||||
clearVNodeObservers(vNode);
|
||||
vNode.observers = null;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createStoreHook<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||
storeHandler: StoreHandler<S, A, C>
|
||||
): () => StoreHandler<S, A, C> {
|
||||
return () => {
|
||||
if (!storeHandler.$config.options?.suppressHooks) {
|
||||
hookStore();
|
||||
}
|
||||
|
||||
return storeHandler;
|
||||
};
|
||||
}
|
||||
|
||||
export function useStore<S extends object, A extends UserActions<S>, C extends UserComputedValues<S>>(
|
||||
id: string
|
||||
): StoreHandler<S, A, C> {
|
||||
const storeObj = storeMap.get(id);
|
||||
|
||||
if (storeObj && !storeObj.$config.options?.suppressHooks) hookStore();
|
||||
|
||||
return storeObj as StoreHandler<S,A,C>;
|
||||
}
|
||||
|
||||
export function clearStore(id:string):void {
|
||||
storeMap.delete(id);
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
export interface IObserver {
|
||||
|
||||
useProp: (key: string) => void;
|
||||
|
||||
addListener: (listener: () => void) => void;
|
||||
|
||||
removeListener: (listener: () => void) => void;
|
||||
|
||||
setProp: (key: string) => void;
|
||||
|
||||
triggerChangeListeners: () => void;
|
||||
|
||||
triggerUpdate: (vNode: any) => void;
|
||||
|
||||
allChange: () => void;
|
||||
|
||||
clearByVNode: (vNode: any) => void;
|
||||
}
|
||||
|
||||
type RemoveFirstFromTuple<T extends any[]> =
|
||||
T['length'] extends 0 ? [] :
|
||||
(((...b: T) => void) extends (a, ...b: infer I) => void ? I : [])
|
||||
|
||||
|
||||
type UserActions<S extends object> = { [K:string]: ActionFunction<S> };
|
||||
type UserComputedValues<S extends object> = { [K:string]: ComputedFunction<S> };
|
||||
|
||||
type ActionFunction<S extends object> = (state: S, ...args: any[]) => any;
|
||||
type ComputedFunction<S extends object> = (state: S) => any;
|
||||
type Action<T extends UserActions<?>> = (...args:RemoveFirstFromTuple<Parameters<T>>)=>ReturnType<T>
|
||||
type AsyncAction<T extends UserActions<?>> = (...args:RemoveFirstFromTuple<Parameters<T>>)=>Promise<ReturnType<T>>
|
||||
|
||||
type StoreActions<S extends object,A extends UserActions<S>> = { [K in keyof A]: Action<A[K]> };
|
||||
type QueuedStoreActions<S extends object,A extends UserActions<S>> = { [K in keyof A]: AsyncAction<A[K]> };
|
||||
type ComputedValues<S extends object,C extends UserComputedValues<S>> = { [K in keyof C]: ReturnType<C[K]> };
|
||||
type PostponedAction = (state: object, ...args: any[]) => Promise<any>;
|
||||
type PostponedActions = { [key:string]: PostponedAction }
|
||||
|
||||
export type StoreHandler<S extends object,A extends UserActions<S>,C extends UserComputedValues<S>> =
|
||||
{$subscribe: ((listener: () => void) => void),
|
||||
$unsubscribe: ((listener: () => void) => void),
|
||||
$state: S,
|
||||
$config: StoreConfig<S,A,C>,
|
||||
$queue: QueuedStoreActions<S,A>,
|
||||
$actions: StoreActions<S,A>,
|
||||
$computed: ComputedValues<S,C>,
|
||||
reduxHandler?:ReduxStoreHandler}
|
||||
&
|
||||
{[K in keyof S]: S[K]}
|
||||
&
|
||||
{[K in keyof A]: Action<A[K]>}
|
||||
&
|
||||
{[K in keyof C]: ReturnType<C[K]>}
|
||||
|
||||
export type StoreConfig<S extends object,A extends UserActions<S>,C extends UserComputedValues<S>> = {
|
||||
state?: S,
|
||||
options?:{suppressHooks?: boolean},
|
||||
actions?: A,
|
||||
id?: string,
|
||||
computed?: C
|
||||
}
|
||||
|
||||
type ReduxStoreHandler = {
|
||||
reducer:(state:any,action:{type:string})=>any,
|
||||
dispatch:(action:{type:string})=>void,
|
||||
getState:()=>any,
|
||||
subscribe:(listener:()=>void)=>((listener:()=>void)=>void)
|
||||
replaceReducer: (reducer: (state:any,action:{type:string})=>any)=>void
|
||||
_horizonXstore: StoreHandler
|
||||
}
|
||||
|
||||
type ReduxAction = {
|
||||
type:string
|
||||
}
|
||||
|
||||
type ReduxMiddleware = (store:ReduxStoreHandler, extraArgument?:any) =>
|
||||
(next:((action:ReduxAction)=>any)) =>
|
||||
(action:(
|
||||
ReduxAction|
|
||||
((dispatch:(action:ReduxAction)=>void,store:ReduxStoreHandler,extraArgument?:any)=>any)
|
||||
)) => ReduxStoreHandler
|
|
@ -45,13 +45,28 @@ export function resetContext(providerVNode: VNode) {
|
|||
context.value = providerVNode.context;
|
||||
}
|
||||
|
||||
// 在局部更新时,恢复父节点的context
|
||||
// 在局部更新时,从上到下恢复父节点的context
|
||||
export function recoverParentContext(vNode: VNode) {
|
||||
const contextProviders: VNode[] = [];
|
||||
let parent = vNode.parent;
|
||||
while (parent !== null) {
|
||||
if (parent.tag === ContextProvider) {
|
||||
contextProviders.unshift(parent);
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
contextProviders.forEach(node => {
|
||||
setContext(node, node.props.value);
|
||||
});
|
||||
}
|
||||
|
||||
// 在局部更新时,从下到上重置父节点的context
|
||||
export function resetParentContext(vNode: VNode) {
|
||||
let parent = vNode.parent;
|
||||
|
||||
while (parent !== null) {
|
||||
if (parent.tag === ContextProvider) {
|
||||
setContext(parent, parent.props.value);
|
||||
resetContext(parent);
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
isExecuting,
|
||||
setExecuteMode
|
||||
} from './ExecuteMode';
|
||||
import { recoverParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver';
|
||||
import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver';
|
||||
import {
|
||||
updateChildShouldUpdate,
|
||||
updateParentsChildShouldUpdate,
|
||||
|
@ -43,6 +43,11 @@ let unrecoverableErrorDuringBuild: any = null;
|
|||
|
||||
// 当前运行的vNode节点
|
||||
let processing: VNode | null = null;
|
||||
let currentRoot: VNode | null = null;
|
||||
export function getCurrentRoot() {
|
||||
return currentRoot;
|
||||
}
|
||||
|
||||
export function setProcessing(vNode: VNode | null) {
|
||||
processing = vNode;
|
||||
}
|
||||
|
@ -258,6 +263,10 @@ function buildVNodeTree(treeRoot: VNode) {
|
|||
handleError(treeRoot, thrownValue);
|
||||
}
|
||||
}
|
||||
if (startVNode.tag !== TreeRoot) { // 不是根节点
|
||||
// 恢复父节点的context
|
||||
resetParentContext(startVNode);
|
||||
}
|
||||
|
||||
setProcessingClassVNode(null);
|
||||
|
||||
|
@ -267,7 +276,7 @@ function buildVNodeTree(treeRoot: VNode) {
|
|||
// 总体任务入口
|
||||
function renderFromRoot(treeRoot) {
|
||||
runAsyncEffects();
|
||||
|
||||
currentRoot = treeRoot;
|
||||
// 1. 构建vNode树
|
||||
buildVNodeTree(treeRoot);
|
||||
|
||||
|
@ -278,6 +287,7 @@ function renderFromRoot(treeRoot) {
|
|||
|
||||
// 2. 提交变更
|
||||
submitToRender(treeRoot);
|
||||
currentRoot = null;
|
||||
|
||||
if (window.__HORIZON_DEV_HOOK__) {
|
||||
const hook = window.__HORIZON_DEV_HOOK__;
|
||||
|
|
|
@ -74,7 +74,11 @@ export class VNode {
|
|||
suspenseState: SuspenseState;
|
||||
|
||||
path = ''; // 保存从根到本节点的路径
|
||||
|
||||
// 根节点数据
|
||||
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
|
||||
delegatedEvents: Set<string>
|
||||
delegatedNativeEvents: Set<string>
|
||||
|
||||
belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode,处理ref的时候使用
|
||||
|
||||
|
@ -94,6 +98,8 @@ export class VNode {
|
|||
this.realNode = realNode;
|
||||
this.task = null;
|
||||
this.toUpdateNodes = new Set<VNode>();
|
||||
this.delegatedEvents = new Set<string>();
|
||||
this.delegatedNativeEvents = new Set<string>();
|
||||
this.updates = null;
|
||||
this.stateCallbacks = null;
|
||||
this.state = null;
|
||||
|
|
|
@ -31,7 +31,6 @@ export function travelVNodeTree(
|
|||
finishVNode: VNode, // 结束遍历节点,有时候和beginVNode不相同
|
||||
handleWhenToParent: Function | null
|
||||
): VNode | null {
|
||||
const filter = childFilter === null;
|
||||
let node = beginVNode;
|
||||
|
||||
while (true) {
|
||||
|
@ -43,7 +42,7 @@ export function travelVNodeTree(
|
|||
|
||||
// 找子节点
|
||||
const childVNode = node.child;
|
||||
if (childVNode !== null && (filter || !childFilter(node))) {
|
||||
if (childVNode !== null && (childFilter === null || !childFilter(node))) {
|
||||
childVNode.parent = node;
|
||||
node = childVNode;
|
||||
continue;
|
||||
|
@ -194,20 +193,6 @@ export function getSiblingDom(vNode: VNode): Element | null {
|
|||
}
|
||||
}
|
||||
|
||||
function isSameContainer(
|
||||
container: Element,
|
||||
targetContainer: EventTarget,
|
||||
): boolean {
|
||||
if (container === targetContainer) {
|
||||
return true;
|
||||
}
|
||||
// 注释类型的节点
|
||||
if (isComment(container) && container.parentNode === targetContainer) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isPortalRoot(vNode, targetContainer) {
|
||||
if (vNode.tag === DomPortal) {
|
||||
let topVNode = vNode.parent;
|
||||
|
@ -216,7 +201,7 @@ function isPortalRoot(vNode, targetContainer) {
|
|||
if (grandTag === TreeRoot || grandTag === DomPortal) {
|
||||
const topContainer = topVNode.realNode;
|
||||
// 如果topContainer是targetContainer,不需要在这里处理
|
||||
if (isSameContainer(topContainer, targetContainer)) {
|
||||
if (topContainer === targetContainer) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -228,28 +213,28 @@ function isPortalRoot(vNode, targetContainer) {
|
|||
}
|
||||
|
||||
// 获取根vNode节点
|
||||
export function getExactNode(targetVNode, targetContainer) {
|
||||
export function findRoot(targetVNode, targetDom) {
|
||||
// 确认vNode节点是否准确,portal场景下可能祖先节点不准确
|
||||
let vNode = targetVNode;
|
||||
while (vNode !== null) {
|
||||
if (vNode.tag === TreeRoot || vNode.tag === DomPortal) {
|
||||
let container = vNode.realNode;
|
||||
if (isSameContainer(container, targetContainer)) {
|
||||
let dom = vNode.realNode;
|
||||
if (dom === targetDom) {
|
||||
break;
|
||||
}
|
||||
if (isPortalRoot(vNode, targetContainer)) {
|
||||
if (isPortalRoot(vNode, targetDom)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
while (container !== null) {
|
||||
const parentNode = getNearestVNode(container);
|
||||
while (dom !== null) {
|
||||
const parentNode = getNearestVNode(dom);
|
||||
if (parentNode === null) {
|
||||
return null;
|
||||
}
|
||||
if (parentNode.tag === DomComponent || parentNode.tag === DomText) {
|
||||
return getExactNode(parentNode, targetContainer);
|
||||
return findRoot(parentNode, targetDom);
|
||||
}
|
||||
container = container.parentNode;
|
||||
dom = dom.parentNode;
|
||||
}
|
||||
}
|
||||
vNode = vNode.parent;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"scripts": {
|
||||
"lint": "eslint . --ext .ts",
|
||||
"build": " rollup --config ./scripts/rollup/rollup.config.js",
|
||||
"build:watch": " rollup --watch --config ./scripts/rollup/rollup.config.js",
|
||||
"build-3rdLib": "node ./scripts/gen3rdLib.js",
|
||||
"build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev",
|
||||
"build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon",
|
||||
|
|
|
@ -358,4 +358,79 @@ describe('Context Test', () => {
|
|||
Horizon.render(<App num={8} type={'typeR'} />, container);
|
||||
expect(container.querySelector('p').innerHTML).toBe('Num: 8, Type: typeR');
|
||||
});
|
||||
|
||||
// antd menu 级连context场景,menu路径使用级联context实现
|
||||
it('nested context', () => {
|
||||
const NestedContext = Horizon.createContext([]);
|
||||
let updateContext;
|
||||
|
||||
function App() {
|
||||
const [state, useState] = Horizon.useState([]);
|
||||
updateContext = useState;
|
||||
return (
|
||||
<NestedContext.Provider value={state}>
|
||||
<Sub1 />
|
||||
<Sub2 />
|
||||
</NestedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const div1Ref = Horizon.createRef();
|
||||
const div2Ref = Horizon.createRef();
|
||||
|
||||
let updateSub1;
|
||||
function Sub1() {
|
||||
const path = Horizon.useContext(NestedContext);
|
||||
const [_, setState] = Horizon.useState({});
|
||||
updateSub1 = () => setState({});
|
||||
return (
|
||||
<NestedContext.Provider value={[...path, 1]}>
|
||||
<Son divRef={div1Ref} />
|
||||
</NestedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sub2() {
|
||||
const path = Horizon.useContext(NestedContext);
|
||||
|
||||
return (
|
||||
<NestedContext.Provider value={[...path, 2]}>
|
||||
<Sub3 />
|
||||
</NestedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Sub3() {
|
||||
const path = Horizon.useContext(NestedContext);
|
||||
|
||||
return (
|
||||
<NestedContext.Provider value={[...path, 3]}>
|
||||
<Son divRef={div2Ref} />
|
||||
</NestedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function Son({ divRef }) {
|
||||
const path = Horizon.useContext(NestedContext);
|
||||
return (
|
||||
<NestedContext.Provider value={path}>
|
||||
<div ref={divRef}>{path.join(',')}</div>
|
||||
</NestedContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
updateSub1();
|
||||
expect(div1Ref.current.innerHTML).toEqual('1');
|
||||
expect(div2Ref.current.innerHTML).toEqual('2,3');
|
||||
|
||||
updateContext([0]);
|
||||
expect(div1Ref.current.innerHTML).toEqual('0,1');
|
||||
expect(div2Ref.current.innerHTML).toEqual('0,2,3');
|
||||
|
||||
// 局部更新Sub1
|
||||
updateSub1();
|
||||
expect(div1Ref.current.innerHTML).toEqual('0,1');
|
||||
expect(div2Ref.current.innerHTML).toEqual('0,2,3');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,6 +30,7 @@ describe('useEffect Hook Test', () => {
|
|||
expect(document.getElementById('p').style.display).toBe('block');
|
||||
// 点击按钮触发num加1
|
||||
container.querySelector('button').click();
|
||||
|
||||
expect(document.getElementById('p').style.display).toBe('none');
|
||||
container.querySelector('button').click();
|
||||
expect(container.querySelector('p').style.display).toBe('inline');
|
||||
|
|
|
@ -61,4 +61,10 @@ describe('Dom Attribute', () => {
|
|||
container.querySelector('div').setAttribute('data-first-name', 'Tom');
|
||||
expect(container.querySelector('div').dataset.firstName).toBe('Tom');
|
||||
});
|
||||
|
||||
it('style 自动加px', () => {
|
||||
const div = Horizon.render(<div style={{width: 10, height: 20}}/>, container);
|
||||
expect(window.getComputedStyle(div).getPropertyValue('width')).toBe('10px');
|
||||
expect(window.getComputedStyle(div).getPropertyValue('height')).toBe('20px');
|
||||
});
|
||||
});
|
|
@ -22,16 +22,6 @@ describe('Dom Input', () => {
|
|||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('checked属性受控时无法更改', () => {
|
||||
Horizon.render(<input type='checkbox' checked={true} onChange={() => {
|
||||
LogUtils.log('checkbox click');
|
||||
}} />, container);
|
||||
container.querySelector('input').click();
|
||||
// 点击复选框不会改变checked的值
|
||||
expect(LogUtils.getAndClear()).toEqual(['checkbox click']);
|
||||
expect(container.querySelector('input').checked).toBe(true);
|
||||
});
|
||||
|
||||
it('复选框的value属性值可以改变', () => {
|
||||
Horizon.render(
|
||||
<input type='checkbox' value='' onChange={() => {
|
||||
|
@ -96,30 +86,6 @@ describe('Dom Input', () => {
|
|||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('value属性受控时无法更改', () => {
|
||||
const realNode = Horizon.render(<input type='text' value={'text'} onChange={() => {
|
||||
LogUtils.log('text change');
|
||||
}} />, container);
|
||||
|
||||
// 模拟改变text输入框的值
|
||||
// 先修改
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
'value',
|
||||
).set.call(realNode, 'abcd');
|
||||
// 再触发事件
|
||||
realNode.dispatchEvent(
|
||||
new Event('input', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
// 确实发生了input事件
|
||||
expect(LogUtils.getAndClear()).toEqual(['text change']);
|
||||
// value受控,不会改变
|
||||
expect(container.querySelector('input').value).toBe('text');
|
||||
});
|
||||
|
||||
it('value值会转为字符串', () => {
|
||||
const realNode = Horizon.render(<input type='text' value={1} />, container);
|
||||
expect(realNode.value).toBe('1');
|
||||
|
@ -172,6 +138,11 @@ describe('Dom Input', () => {
|
|||
expect(realNode.getAttribute('value')).toBe('default');
|
||||
});
|
||||
|
||||
it('value为0、defaultValue为1,input 的value应该为0', () => {
|
||||
const input = Horizon.render(<input defaultValue={1} value={0} />, container);
|
||||
expect(input.getAttribute('value')).toBe('0');
|
||||
});
|
||||
|
||||
it('name属性', () => {
|
||||
let realNode = Horizon.render(<input type='text' name={'name'} />, container);
|
||||
expect(realNode.name).toBe('name');
|
||||
|
@ -244,30 +215,6 @@ describe('Dom Input', () => {
|
|||
expect(document.getElementById('d').checked).toBe(true);
|
||||
});
|
||||
|
||||
it('受控radio的状态', () => {
|
||||
Horizon.render(
|
||||
<>
|
||||
<input type='radio' name='a' checked={true} />
|
||||
<input id='b' type='radio' name='a' checked={false} />
|
||||
</>, container);
|
||||
expect(container.querySelector('input').checked).toBe(true);
|
||||
expect(document.getElementById('b').checked).toBe(false);
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
'checked',
|
||||
).set.call(document.getElementById('b'), true);
|
||||
// 再触发事件
|
||||
document.getElementById('b').dispatchEvent(
|
||||
new Event('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
// 模拟点击单选框B,两个受控radio的状态不会改变
|
||||
expect(container.querySelector('input').checked).toBe(true);
|
||||
expect(document.getElementById('b').checked).toBe(false);
|
||||
});
|
||||
|
||||
it('name改变不影响相同name的radio', () => {
|
||||
const inputRef = Horizon.createRef();
|
||||
const App = () => {
|
||||
|
|
|
@ -53,37 +53,6 @@ describe('Dom Select', () => {
|
|||
expect(realNode.value).toBe('React');
|
||||
});
|
||||
|
||||
it('受控select', () => {
|
||||
const selectNode = (
|
||||
<select value='Vue'>
|
||||
<option value='React'>React.js</option>
|
||||
<option value='Vue'>Vue.js</option>
|
||||
<option value='Angular'>Angular.js</option>
|
||||
</select>
|
||||
);
|
||||
const realNode = Horizon.render(selectNode, container);
|
||||
expect(realNode.value).toBe('Vue');
|
||||
expect(realNode.options[1].selected).toBe(true);
|
||||
// 先修改
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLSelectElement.prototype,
|
||||
'value',
|
||||
).set.call(realNode, 'React');
|
||||
// 再触发事件
|
||||
container.querySelector('select').dispatchEvent(
|
||||
new Event('change', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
// 鼠标改变受控select不生效
|
||||
Horizon.render(selectNode, container);
|
||||
// 'React'项没有被选中
|
||||
expect(realNode.options[0].selected).toBe(false);
|
||||
expect(realNode.options[1].selected).toBe(true);
|
||||
expect(realNode.value).toBe('Vue');
|
||||
});
|
||||
|
||||
it('受控select转为不受控会保存原来select', () => {
|
||||
const selectNode = (
|
||||
<select value='Vue'>
|
||||
|
|
|
@ -38,26 +38,6 @@ describe('Dom Textarea', () => {
|
|||
expect(realNode.value).toBe('React');
|
||||
});
|
||||
|
||||
it('受控组件value不变', () => {
|
||||
let realNode = Horizon.render(<textarea value='text' />, container);
|
||||
expect(realNode.getAttribute('value')).toBe(null);
|
||||
expect(realNode.value).toBe('text');
|
||||
// 先修改
|
||||
Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value',
|
||||
).set.call(realNode, 'textabc');
|
||||
// 再触发事件
|
||||
container.querySelector('textarea').dispatchEvent(
|
||||
new Event('change', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
);
|
||||
// 组件受控,想要改变value,需要通过onChange改变state
|
||||
expect(realNode.value).toBe('text');
|
||||
});
|
||||
|
||||
it('设置defaultValue', () => {
|
||||
let defaultVal = 'Vue';
|
||||
const textareaNode = <textarea defaultValue={defaultVal} />;
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import * as TestUtils from '../jest/testUtils';
|
||||
|
||||
function dispatchChangeEvent(input) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
||||
nativeInputValueSetter.call(input, 'test');
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
|
||||
describe('事件', () => {
|
||||
const LogUtils = TestUtils.getLogUtils();
|
||||
it('根节点挂载全量事件', () => {
|
||||
|
@ -34,7 +41,7 @@ describe('事件', () => {
|
|||
'btn capture',
|
||||
'btn bubble',
|
||||
'p bubble',
|
||||
'div bubble'
|
||||
'div bubble',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -46,14 +53,14 @@ describe('事件', () => {
|
|||
keyCode = e.keyCode;
|
||||
}}
|
||||
/>,
|
||||
container,
|
||||
container
|
||||
);
|
||||
node.dispatchEvent(
|
||||
new KeyboardEvent('keypress', {
|
||||
keyCode: 65,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(keyCode).toBe(65);
|
||||
});
|
||||
|
@ -64,7 +71,10 @@ describe('事件', () => {
|
|||
<>
|
||||
<div onClickCapture={() => LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>
|
||||
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
|
||||
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={(e) => TestUtils.stopBubbleOrCapture(e, 'btn bubble')} />
|
||||
<button
|
||||
onClickCapture={() => LogUtils.log('btn capture')}
|
||||
onClick={e => TestUtils.stopBubbleOrCapture(e, 'btn bubble')}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
@ -78,7 +88,7 @@ describe('事件', () => {
|
|||
'div capture',
|
||||
'p capture',
|
||||
'btn capture',
|
||||
'btn bubble'
|
||||
'btn bubble',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -86,7 +96,10 @@ describe('事件', () => {
|
|||
const App = () => {
|
||||
return (
|
||||
<>
|
||||
<div onClickCapture={(e) => TestUtils.stopBubbleOrCapture(e, 'div capture')} onClick={() => LogUtils.log('div bubble')}>
|
||||
<div
|
||||
onClickCapture={e => TestUtils.stopBubbleOrCapture(e, 'div capture')}
|
||||
onClick={() => LogUtils.log('div bubble')}
|
||||
>
|
||||
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
|
||||
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={() => LogUtils.log('btn bubble')} />
|
||||
</p>
|
||||
|
@ -99,7 +112,7 @@ describe('事件', () => {
|
|||
|
||||
expect(LogUtils.getAndClear()).toEqual([
|
||||
// 阻止捕获,不再继续向下执行
|
||||
'div capture'
|
||||
'div capture',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -114,19 +127,148 @@ describe('事件', () => {
|
|||
);
|
||||
};
|
||||
Horizon.render(<App />, container);
|
||||
container.querySelector('div').addEventListener('click', () => {
|
||||
LogUtils.log('div bubble');
|
||||
}, false);
|
||||
container.querySelector('p').addEventListener('click', () => {
|
||||
LogUtils.log('p bubble');
|
||||
}, false);
|
||||
container.querySelector('button').addEventListener('click', (e) => {
|
||||
LogUtils.log('btn bubble');
|
||||
e.stopPropagation();
|
||||
}, false);
|
||||
container.querySelector('div').addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
LogUtils.log('div bubble');
|
||||
},
|
||||
false
|
||||
);
|
||||
container.querySelector('p').addEventListener(
|
||||
'click',
|
||||
() => {
|
||||
LogUtils.log('p bubble');
|
||||
},
|
||||
false
|
||||
);
|
||||
container.querySelector('button').addEventListener(
|
||||
'click',
|
||||
e => {
|
||||
LogUtils.log('btn bubble');
|
||||
e.stopPropagation();
|
||||
},
|
||||
false
|
||||
);
|
||||
container.querySelector('button').click();
|
||||
expect(LogUtils.getAndClear()).toEqual([
|
||||
'btn bubble'
|
||||
]);
|
||||
expect(LogUtils.getAndClear()).toEqual(['btn bubble']);
|
||||
});
|
||||
|
||||
it('动态增加事件', () => {
|
||||
let update;
|
||||
let inputRef = Horizon.createRef();
|
||||
|
||||
function Test() {
|
||||
const [inputProps, setProps] = Horizon.useState({});
|
||||
update = setProps;
|
||||
return <input ref={inputRef} {...inputProps} />;
|
||||
}
|
||||
|
||||
Horizon.render(<Test />, container);
|
||||
update({
|
||||
onChange: () => {
|
||||
LogUtils.log('change');
|
||||
},
|
||||
});
|
||||
dispatchChangeEvent(inputRef.current);
|
||||
|
||||
expect(LogUtils.getAndClear()).toEqual(['change']);
|
||||
});
|
||||
|
||||
it('Radio change事件', () => {
|
||||
let radio1Called = 0;
|
||||
let radio2Called = 0;
|
||||
|
||||
function onChange1() {
|
||||
radio1Called++;
|
||||
}
|
||||
|
||||
function onChange2() {
|
||||
radio2Called++;
|
||||
}
|
||||
|
||||
const radio1Ref = Horizon.createRef();
|
||||
const radio2Ref = Horizon.createRef();
|
||||
|
||||
Horizon.render(
|
||||
<>
|
||||
<input type="radio" ref={radio1Ref} name="name" onChange={onChange1} />
|
||||
<input type="radio" ref={radio2Ref} name="name" onChange={onChange2} />
|
||||
</>,
|
||||
container
|
||||
);
|
||||
|
||||
function clickRadioAndExpect(radio, [expect1, expect2]) {
|
||||
radio.click();
|
||||
expect(radio1Called).toBe(expect1);
|
||||
expect(radio2Called).toBe(expect2);
|
||||
}
|
||||
|
||||
// 先选择选项1
|
||||
clickRadioAndExpect(radio1Ref.current, [1, 0]);
|
||||
|
||||
// 再选择选项1
|
||||
clickRadioAndExpect(radio2Ref.current, [1, 1]);
|
||||
|
||||
// 先选择选项1,radio1应该重新触发onchange
|
||||
clickRadioAndExpect(radio1Ref.current, [2, 1]);
|
||||
});
|
||||
|
||||
it('多根节点下,事件挂载正确', () => {
|
||||
const root1 = document.createElement('div');
|
||||
const root2 = document.createElement('div');
|
||||
root1.key = 'root1';
|
||||
root2.key = 'root2';
|
||||
let input1, input2, update1, update2;
|
||||
|
||||
function App1() {
|
||||
const [props, setProps] = Horizon.useState({});
|
||||
update1 = setProps;
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
ref={n => (input1 = n)}
|
||||
onChange={() => {
|
||||
LogUtils.log('input1 changed');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function App2() {
|
||||
const [props, setProps] = Horizon.useState({});
|
||||
update2 = setProps;
|
||||
|
||||
return (
|
||||
<input
|
||||
{...props}
|
||||
ref={n => (input2 = n)}
|
||||
onChange={() => {
|
||||
LogUtils.log('input2 changed');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 多根mount阶段挂载onChange事件
|
||||
Horizon.render(<App1 key={1} />, root1);
|
||||
Horizon.render(<App2 key={2} />, root2);
|
||||
|
||||
dispatchChangeEvent(input1);
|
||||
expect(LogUtils.getAndClear()).toEqual(['input1 changed']);
|
||||
dispatchChangeEvent(input2);
|
||||
expect(LogUtils.getAndClear()).toEqual(['input2 changed']);
|
||||
|
||||
// 多根update阶段挂载onClick事件
|
||||
update1({
|
||||
onClick: () => LogUtils.log('input1 clicked'),
|
||||
});
|
||||
update2({
|
||||
onClick: () => LogUtils.log('input2 clicked'),
|
||||
});
|
||||
|
||||
input1.click();
|
||||
expect(LogUtils.getAndClear()).toEqual(['input1 clicked']);
|
||||
input2.click();
|
||||
expect(LogUtils.getAndClear()).toEqual(['input2 clicked']);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的Array', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = [
|
||||
{ name: 'p1', age: 1 },
|
||||
{ name: 'p2', age: 2 },
|
||||
];
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.push(person);
|
||||
},
|
||||
delOnePerson: state => {
|
||||
state.persons.pop();
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addOnePerson = function() {
|
||||
userStore.addOnePerson(newPerson);
|
||||
};
|
||||
const delOnePerson = function() {
|
||||
userStore.delOnePerson();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试Array方法: push()、pop()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.length}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
|
||||
// 在Array中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
|
||||
|
||||
// 在Array中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
|
||||
});
|
||||
|
||||
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
|
||||
let globalStore = null;
|
||||
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
globalStore = userStore;
|
||||
|
||||
const nameList = [];
|
||||
const entries = userStore.$state.persons?.entries();
|
||||
if (entries) {
|
||||
for (const entry of entries) {
|
||||
nameList.push(entry[1].name);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// push
|
||||
globalStore.$state.persons.push(newPerson);
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
|
||||
|
||||
// 重新赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
|
||||
|
||||
// unshift
|
||||
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
|
||||
|
||||
// 重新赋值 null
|
||||
globalStore.$state.persons = null;
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
|
||||
// 重新赋值 [{ name: 'p1', age: 1 }]
|
||||
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
|
||||
});
|
||||
|
||||
it('测试Array方法: forEach()', () => {
|
||||
let globalStore = null;
|
||||
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
globalStore = userStore;
|
||||
|
||||
const nameList = [];
|
||||
userStore.$state.persons?.forEach(per => {
|
||||
nameList.push(per.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// push
|
||||
globalStore.$state.persons.push(newPerson);
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
|
||||
|
||||
// 重新赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
|
||||
|
||||
// unshift
|
||||
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
|
||||
|
||||
// 重新赋值 null
|
||||
globalStore.$state.persons = null;
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
|
||||
// 重新赋值 [{ name: 'p1', age: 1 }]
|
||||
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,323 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的Map', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new Map([
|
||||
['p1', 1],
|
||||
['p2', 2],
|
||||
]);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.set(person.name, person.age);
|
||||
},
|
||||
delOnePerson: (state, person) => {
|
||||
state.persons.delete(person.name);
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addOnePerson = function() {
|
||||
userStore.addOnePerson(newPerson);
|
||||
};
|
||||
const delOnePerson = function() {
|
||||
userStore.delOnePerson(newPerson);
|
||||
};
|
||||
const clearPersons = function() {
|
||||
userStore.clearPersons();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<button id={'clearBtn'} onClick={clearPersons}>
|
||||
clear persons
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试Map方法: set()、delete()、clear()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
|
||||
});
|
||||
|
||||
it('测试Map方法: keys()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
const keys = userStore.$state.persons.keys();
|
||||
for (const key of keys) {
|
||||
nameList.push(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: values()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const ageList = [];
|
||||
const values = userStore.$state.persons.values();
|
||||
for (const val of values) {
|
||||
ageList.push(val);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'ageList'} text={`age list: ${ageList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: entries()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
const entries = userStore.$state.persons.entries();
|
||||
for (const entry of entries) {
|
||||
nameList.push(entry[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: forEach()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
userStore.$state.persons.forEach((val, key) => {
|
||||
nameList.push(key);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: has()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson.name)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
|
||||
});
|
||||
|
||||
it('测试Map方法: for of()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
for (const per of userStore.$state.persons) {
|
||||
nameList.push(per[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,164 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的混合类型变化', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new Set([{ name: 'p1', age: 1, love: new Map() }]);
|
||||
persons.add({
|
||||
name: 'p2',
|
||||
age: 2,
|
||||
love: new Map(),
|
||||
});
|
||||
persons
|
||||
.values()
|
||||
.next()
|
||||
.value.love.set('lanqiu', { moneny: 100, days: [1, 3, 5] });
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addDay: (state, day) => {
|
||||
state.persons
|
||||
.values()
|
||||
.next()
|
||||
.value.love.get('lanqiu')
|
||||
.days.push(day);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addDay = function() {
|
||||
userStore.addDay(7);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addDay}>
|
||||
add day
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试state -> set -> map -> array的数据变化', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const days = userStore.$state.persons
|
||||
.values()
|
||||
.next()
|
||||
.value.love.get('lanqiu').days;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'dayList'} text={`love: ${days.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5');
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#dayList').innerHTML).toBe('love: 1 3 5 7');
|
||||
});
|
||||
|
||||
it('属性是个class实例', () => {
|
||||
class Person {
|
||||
name;
|
||||
age;
|
||||
loves = new Set();
|
||||
|
||||
constructor(name, age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
setName(name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
setAge(age) {
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
getAge() {
|
||||
return this.age;
|
||||
}
|
||||
|
||||
addLove(lv) {
|
||||
this.loves.add(lv);
|
||||
}
|
||||
|
||||
getLoves() {
|
||||
return this.loves;
|
||||
}
|
||||
}
|
||||
|
||||
let globalPerson;
|
||||
let globalStore;
|
||||
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
globalStore = userStore;
|
||||
|
||||
const nameList = [];
|
||||
const valIterator = userStore.$state.persons.values();
|
||||
let per = valIterator.next();
|
||||
while (!per.done) {
|
||||
nameList.push(per.value.name ?? per.value.getName());
|
||||
globalPerson = per.value;
|
||||
per = valIterator.next();
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={nameList.join(' ')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2');
|
||||
|
||||
// 动态增加一个Person实例
|
||||
globalStore.$state.persons.add(new Person('ClassPerson', 5));
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson');
|
||||
|
||||
globalPerson.setName('ClassPerson1');
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('p1 p2 ClassPerson1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,294 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的Set', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new Set([
|
||||
{ name: 'p1', age: 1 },
|
||||
{ name: 'p2', age: 2 },
|
||||
]);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.add(person);
|
||||
},
|
||||
delOnePerson: (state, person) => {
|
||||
state.persons.delete(person);
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addOnePerson = function() {
|
||||
userStore.addOnePerson(newPerson);
|
||||
};
|
||||
const delOnePerson = function() {
|
||||
userStore.delOnePerson(newPerson);
|
||||
};
|
||||
const clearPersons = function() {
|
||||
userStore.clearPersons();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<button id={'clearBtn'} onClick={clearPersons}>
|
||||
clear persons
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试Set方法: add()、delete()、clear()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
const personArr = Array.from(userStore.$state.persons);
|
||||
const nameList = [];
|
||||
const keys = userStore.$state.persons.keys();
|
||||
for (const key of keys) {
|
||||
nameList.push(key.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'size'} text={`persons number: ${userStore.$state.persons.size}`} />
|
||||
<Text id={'lastAge'} text={`last person age: ${personArr[personArr.length - 1]?.age ?? 0}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 2');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
|
||||
|
||||
// 在set中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
|
||||
// clear set
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
|
||||
expect(container.querySelector('#lastAge').innerHTML).toBe('last person age: 0');
|
||||
});
|
||||
|
||||
it('测试Set方法: keys()、values()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
const keys = userStore.$state.persons.keys();
|
||||
// const keys = userStore.$state.persons.values();
|
||||
for (const key of keys) {
|
||||
nameList.push(key.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在set中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear set
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Set方法: entries()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
const entries = userStore.$state.persons.entries();
|
||||
for (const entry of entries) {
|
||||
nameList.push(entry[0].name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在set中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear set
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Set方法: forEach()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
userStore.$state.persons.forEach(per => {
|
||||
nameList.push(per.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在set中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear set
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Set方法: has()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
|
||||
});
|
||||
|
||||
it('测试Set方法: for of()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
const nameList = [];
|
||||
for (const per of userStore.$state.persons) {
|
||||
nameList.push(per.name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在set中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在set中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear set
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,124 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的WeakMap', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new WeakMap([
|
||||
[{ name: 'p1' }, 1],
|
||||
[{ name: 'p2' }, 2],
|
||||
]);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.set(person, 3);
|
||||
},
|
||||
delOnePerson: (state, person) => {
|
||||
state.persons.delete(person);
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3' };
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addOnePerson = function() {
|
||||
userStore.addOnePerson(newPerson);
|
||||
};
|
||||
const delOnePerson = function() {
|
||||
userStore.delOnePerson(newPerson);
|
||||
};
|
||||
const clearPersons = function() {
|
||||
userStore.clearPersons();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<button id={'clearBtn'} onClick={clearPersons}>
|
||||
clear persons
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试WeakMap方法: set()、delete()、has()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
// 在WeakMap中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
|
||||
|
||||
// 在WeakMap中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
});
|
||||
|
||||
it('测试WeakMap方法: get()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.get(newPerson)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: undefined');
|
||||
// 在WeakMap中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试store中的WeakSet', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new WeakSet([
|
||||
{ name: 'p1', age: 1 },
|
||||
{ name: 'p2', age: 2 },
|
||||
]);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.add(person);
|
||||
},
|
||||
delOnePerson: (state, person) => {
|
||||
state.persons.delete(person);
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
function Parent(props) {
|
||||
const userStore = useStore('user');
|
||||
const addOnePerson = function() {
|
||||
userStore.addOnePerson(newPerson);
|
||||
};
|
||||
const delOnePerson = function() {
|
||||
userStore.delOnePerson(newPerson);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
it('测试WeakSet方法: add()、delete()、has()', () => {
|
||||
function Child(props) {
|
||||
const userStore = useStore('user');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${userStore.$state.persons.has(newPerson)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
// 在WeakSet中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
|
||||
|
||||
// 在WeakSet中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,161 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
function postpone(timer, func) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(function() {
|
||||
resolve(func());
|
||||
}, timer);
|
||||
});
|
||||
}
|
||||
|
||||
describe('Asynchronous functions', () => {
|
||||
let container = null;
|
||||
|
||||
const COUNTER_ID = 'counter';
|
||||
const TOGGLE_ID = 'toggle';
|
||||
const TOGGLE_FAST_ID = 'toggleFast';
|
||||
const RESULT_ID = 'result';
|
||||
|
||||
let useAsyncCounter;
|
||||
|
||||
beforeEach(() => {
|
||||
useAsyncCounter = createStore({
|
||||
state: {
|
||||
counter: 0,
|
||||
check: false,
|
||||
},
|
||||
actions: {
|
||||
increment: function(state) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
state.counter++;
|
||||
resolve();
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
toggle: function(state) {
|
||||
state.check = !state.check;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
value: state => {
|
||||
return (state.check ? 'true' : 'false') + state.counter;
|
||||
},
|
||||
},
|
||||
});
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('Should wait for async actions', async () => {
|
||||
jest.useRealTimers();
|
||||
let globalStore;
|
||||
|
||||
function App() {
|
||||
const store = useAsyncCounter();
|
||||
globalStore = store;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.value}</p>
|
||||
<button onClick={store.$queue.increment} id={COUNTER_ID}>
|
||||
add 1
|
||||
</button>
|
||||
<button onClick={store.$queue.toggle} id={TOGGLE_ID}>
|
||||
slow toggle
|
||||
</button>
|
||||
<button onClick={store.toggle} id={TOGGLE_FAST_ID}>
|
||||
fast toggle
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
// initial state
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
|
||||
|
||||
// slow toggle has nothing to wait for, it is resolved immediately
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, TOGGLE_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0');
|
||||
|
||||
// counter increment is slow. slow toggle waits for result
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, COUNTER_ID);
|
||||
});
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, TOGGLE_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true0');
|
||||
|
||||
// fast toggle does not wait for counter and it is resolved immediately
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, TOGGLE_FAST_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
|
||||
|
||||
// at 150ms counter increment will be resolved and slow toggle immediately after
|
||||
const t150 = postpone(150, () => {
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('true1');
|
||||
});
|
||||
|
||||
// before that, two more actions are added to queue - another counter and slow toggle
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, COUNTER_ID);
|
||||
});
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, TOGGLE_ID);
|
||||
});
|
||||
|
||||
// at 250ms they should be already resolved
|
||||
const t250 = postpone(250, () => {
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false2');
|
||||
});
|
||||
|
||||
await Promise.all([t150, t250]);
|
||||
});
|
||||
|
||||
it('call async action by then', async () => {
|
||||
jest.useFakeTimers();
|
||||
let globalStore;
|
||||
|
||||
function App() {
|
||||
const store = useAsyncCounter();
|
||||
globalStore = store;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
// call async action by then
|
||||
globalStore.$queue.increment().then(() => {
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false1');
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('false0');
|
||||
|
||||
// past 150 ms
|
||||
jest.advanceTimersByTime(150);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
import { useLogStore } from './store';
|
||||
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
describe('Basic store manipulation', () => {
|
||||
let container = null;
|
||||
|
||||
const BUTTON_ID = 'btn';
|
||||
const RESULT_ID = 'result';
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('Should use getters', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return <div id={RESULT_ID}>{logStore.length}</div>;
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||
});
|
||||
|
||||
it('Should use actions and update components', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
id={BUTTON_ID}
|
||||
onClick={() => {
|
||||
logStore.addLog('a');
|
||||
}}
|
||||
>
|
||||
add
|
||||
</button>
|
||||
<p id={RESULT_ID}>{logStore.length}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
import { useLogStore } from './store';
|
||||
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
describe('Dollar store access', () => {
|
||||
let container = null;
|
||||
|
||||
const BUTTON_ID = 'btn';
|
||||
const RESULT_ID = 'result';
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('Should use $state and $computed', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return <div id={RESULT_ID}>{logStore.$computed.length()}</div>;
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||
});
|
||||
|
||||
it('Should use $actions and update components', () => {
|
||||
function App() {
|
||||
const logStore = useLogStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
id={BUTTON_ID}
|
||||
onClick={() => {
|
||||
logStore.$actions.addLog();
|
||||
}}
|
||||
>
|
||||
add
|
||||
</button>
|
||||
<p id={RESULT_ID}>{logStore.$computed.length()}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
describe('Self referencing', () => {
|
||||
let container = null;
|
||||
|
||||
const BUTTON_ID = 'btn';
|
||||
const RESULT_ID = 'result';
|
||||
|
||||
const useSelfRefStore = createStore({
|
||||
state: {
|
||||
val: 2,
|
||||
},
|
||||
actions: {
|
||||
magic: function(state) {
|
||||
state.val = state.val * 2 - 1;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
value: state => state.val,
|
||||
double: function() {
|
||||
return this.value * 2;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('Should use own getters', () => {
|
||||
function App() {
|
||||
const store = useSelfRefStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.double}</p>
|
||||
<button onClick={store.magic} id={BUTTON_ID}>
|
||||
do magic
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('4');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('6');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('10');
|
||||
});
|
||||
|
||||
it('should access other stores', () => {
|
||||
const useOtherStore = createStore({
|
||||
state: {},
|
||||
actions: {
|
||||
doMagic: () => useSelfRefStore().magic(),
|
||||
},
|
||||
computed: {
|
||||
magicConstant: () => useSelfRefStore().value,
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const store = useOtherStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.magicConstant}</p>
|
||||
<button onClick={store.doMagic} id={BUTTON_ID}>
|
||||
do magic
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('5');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('9');
|
||||
});
|
||||
|
||||
it('should use parametric getters', () => {
|
||||
const useArrayStore = createStore({
|
||||
state: {
|
||||
items: ['a', 'b', 'c'],
|
||||
},
|
||||
actions: {
|
||||
setItem: (state, index, value) => (state.items[index] = value),
|
||||
},
|
||||
computed: {
|
||||
getItem: state => index => state.items[index],
|
||||
},
|
||||
});
|
||||
|
||||
function App() {
|
||||
const store = useArrayStore();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.getItem(0) + store.getItem(1) + store.getItem(2)}</p>
|
||||
<button
|
||||
id={BUTTON_ID}
|
||||
onClick={() => {
|
||||
store.setItem(0, 'd');
|
||||
store.setItem(1, 'e');
|
||||
store.setItem(2, 'f');
|
||||
}}
|
||||
>
|
||||
change
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('abc');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('def');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
|
||||
describe('Reset', () => {
|
||||
it('RESET NOT IMPLEMENTED', async () => {
|
||||
// console.log('reset functionality is not yet implemented')
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
return;
|
||||
|
||||
let container = null;
|
||||
|
||||
const BUTTON_ID = 'btn';
|
||||
const RESET_ID = 'reset';
|
||||
const RESULT_ID = 'result';
|
||||
|
||||
const useCounter = createStore({
|
||||
state: {
|
||||
counter: 0,
|
||||
},
|
||||
actions: {
|
||||
increment: function(state) {
|
||||
state.counter++;
|
||||
},
|
||||
},
|
||||
computed: {},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
});
|
||||
|
||||
it('Should reset to default state', async () => {
|
||||
function App() {
|
||||
const store = useCounter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT_ID}>{store.$state.counter}</p>
|
||||
<button onClick={store.increment} id={BUTTON_ID}>
|
||||
add
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
store.$reset();
|
||||
}}
|
||||
id={RESET_ID}
|
||||
>
|
||||
reset
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('2');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, RESET_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('0');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, BUTTON_ID);
|
||||
});
|
||||
|
||||
expect(document.getElementById(RESULT_ID).innerHTML).toBe('1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import { createStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
|
||||
export const useLogStore = createStore({
|
||||
id: 'logStore', // you do not need to specify ID for local store
|
||||
state: {
|
||||
logs: ['log'],
|
||||
},
|
||||
actions: {
|
||||
addLog: (state, data) => {
|
||||
state.logs.push(data);
|
||||
},
|
||||
removeLog: (state, index) => {
|
||||
state.logs.splice(index, 1);
|
||||
},
|
||||
cleanLog: state => {
|
||||
state.logs.length = 0;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
length: state => {
|
||||
return state.logs.length;
|
||||
},
|
||||
log: state => index => state.logs[index],
|
||||
},
|
||||
});
|
|
@ -0,0 +1,208 @@
|
|||
import {
|
||||
createStore,
|
||||
applyMiddleware,
|
||||
combineReducers,
|
||||
bindActionCreators
|
||||
} from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||
|
||||
describe('Redux adapter', () => {
|
||||
it('should use getState()', async () => {
|
||||
const reduxStore = createStore((state, action) => {
|
||||
return state;
|
||||
}, 0);
|
||||
|
||||
expect(reduxStore.getState()).toBe(0);
|
||||
})
|
||||
|
||||
it('Should use default state, dispatch action and update state', async () => {
|
||||
const reduxStore = createStore((state, action) => {
|
||||
switch (action.type) {
|
||||
case('ADD'):
|
||||
return {counter: state.counter + 1}
|
||||
default:
|
||||
return {counter: 0};
|
||||
}
|
||||
});
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(0);
|
||||
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(1);
|
||||
});
|
||||
|
||||
it('Should attach and detach listeners', async () => {
|
||||
let counter = 0;
|
||||
const reduxStore = createStore((state = 0, action) => {
|
||||
switch (action.type) {
|
||||
case('ADD'):
|
||||
return state + 1
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
expect(counter).toBe(0);
|
||||
expect(reduxStore.getState()).toBe(1);
|
||||
const unsubscribe = reduxStore.subscribe(() => {
|
||||
counter++;
|
||||
});
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
expect(counter).toBe(2);
|
||||
expect(reduxStore.getState()).toBe(3);
|
||||
unsubscribe();
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
expect(counter).toBe(2);
|
||||
expect(reduxStore.getState()).toBe(5);
|
||||
});
|
||||
|
||||
it('Should bind action creators', async () => {
|
||||
const addTodo = (text) => {
|
||||
return {
|
||||
type: 'ADD_TODO',
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
const reduxStore = createStore((state = [], action) => {
|
||||
if (action.type === 'ADD_TODO') {
|
||||
return [...state, action.text];
|
||||
}
|
||||
return state;
|
||||
});
|
||||
|
||||
const actions = bindActionCreators({addTodo}, reduxStore.dispatch);
|
||||
|
||||
actions.addTodo('todo');
|
||||
|
||||
expect(reduxStore.getState()[0]).toBe('todo');
|
||||
});
|
||||
|
||||
it('Should replace reducer', async () => {
|
||||
const reduxStore = createStore((state, action) => {
|
||||
switch (action.type) {
|
||||
case('ADD'):
|
||||
return {counter: state.counter + 1}
|
||||
default:
|
||||
return {counter: 0};
|
||||
}
|
||||
});
|
||||
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(1);
|
||||
|
||||
reduxStore.replaceReducer((state, action) => {
|
||||
switch (action.type) {
|
||||
case('SUB'):
|
||||
return {counter: state.counter - 1}
|
||||
default:
|
||||
return {counter: 0};
|
||||
}
|
||||
});
|
||||
|
||||
reduxStore.dispatch({type: 'SUB'});
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(0);
|
||||
})
|
||||
|
||||
it('Should combine reducers', async () => {
|
||||
const booleanReducer = (state = false, action) => {
|
||||
switch (action.type) {
|
||||
case('TOGGLE'):
|
||||
return !state
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const addReducer = (state = 0, action) => {
|
||||
switch (action.type) {
|
||||
case('ADD'):
|
||||
return state + 1
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const reduxStore = createStore(combineReducers({check: booleanReducer, counter: addReducer}));
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(0);
|
||||
expect(reduxStore.getState().check).toBe(false);
|
||||
|
||||
reduxStore.dispatch({type: 'ADD'});
|
||||
reduxStore.dispatch({type: 'TOGGLE'});
|
||||
|
||||
expect(reduxStore.getState().counter).toBe(1);
|
||||
expect(reduxStore.getState().check).toBe(true);
|
||||
});
|
||||
|
||||
it('Should apply enhancers', async () => {
|
||||
let counter = 0;
|
||||
let middlewareCallList = [];
|
||||
|
||||
const callCounter = store => next => action => {
|
||||
middlewareCallList.push('callCounter');
|
||||
counter++;
|
||||
let result = next(action);
|
||||
return result;
|
||||
}
|
||||
|
||||
const reduxStore = createStore((state, action) => {
|
||||
switch (action.type) {
|
||||
case('toggle'):
|
||||
return {
|
||||
check: !state.check
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}, {check: false}, applyMiddleware(callCounter));
|
||||
|
||||
reduxStore.dispatch({type: 'toggle'});
|
||||
reduxStore.dispatch({type: 'toggle'});
|
||||
|
||||
expect(counter).toBe(3); // NOTE: first action is always store initialization
|
||||
});
|
||||
|
||||
it('Should apply multiple enhancers', async () => {
|
||||
let counter = 0;
|
||||
let lastAction = '';
|
||||
let middlewareCallList = [];
|
||||
|
||||
const callCounter = store => next => action => {
|
||||
middlewareCallList.push('callCounter');
|
||||
counter++;
|
||||
let result = next(action);
|
||||
return result;
|
||||
}
|
||||
|
||||
const lastFunctionStorage = store => next => action => {
|
||||
middlewareCallList.push('lastFunctionStorage');
|
||||
lastAction = action.type;
|
||||
let result = next(action);
|
||||
return result;
|
||||
}
|
||||
|
||||
const reduxStore = createStore((state, action) => {
|
||||
switch (action.type) {
|
||||
case('toggle'):
|
||||
return {
|
||||
check: !state.check
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}, {check: false}, applyMiddleware(callCounter, lastFunctionStorage));
|
||||
|
||||
reduxStore.dispatch({type: 'toggle'});
|
||||
|
||||
expect(counter).toBe(2); // NOTE: first action is always store initialization
|
||||
expect(lastAction).toBe('toggle');
|
||||
expect(middlewareCallList[0]).toBe("callCounter");
|
||||
expect(middlewareCallList[1]).toBe("lastFunctionStorage");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
export const ActionType = {
|
||||
Pending: 'PENDING',
|
||||
Fulfilled: 'FULFILLED',
|
||||
Rejected: 'REJECTED',
|
||||
};
|
||||
|
||||
export const promise = store => next => action => {
|
||||
//let result = next(action);
|
||||
store._horizonXstore.$queue.dispatch(action);
|
||||
return result;
|
||||
};
|
||||
|
||||
export function createPromise(config = {}) {
|
||||
const defaultTypes = [ActionType.Pending, ActionType.Fulfilled, ActionType.Rejected];
|
||||
const PROMISE_TYPE_SUFFIXES = config.promiseTypeSuffixes || defaultTypes;
|
||||
const PROMISE_TYPE_DELIMITER = config.promiseTypeDelimiter || '_';
|
||||
|
||||
return store => {
|
||||
const { dispatch } = store;
|
||||
|
||||
return next => action => {
|
||||
/**
|
||||
* Instantiate variables to hold:
|
||||
* (1) the promise
|
||||
* (2) the data for optimistic updates
|
||||
*/
|
||||
let promise;
|
||||
let data;
|
||||
|
||||
/**
|
||||
* There are multiple ways to dispatch a promise. The first step is to
|
||||
* determine if the promise is defined:
|
||||
* (a) explicitly (action.payload.promise is the promise)
|
||||
* (b) implicitly (action.payload is the promise)
|
||||
* (c) as an async function (returns a promise when called)
|
||||
*
|
||||
* If the promise is not defined in one of these three ways, we don't do
|
||||
* anything and move on to the next middleware in the middleware chain.
|
||||
*/
|
||||
|
||||
// Step 1a: Is there a payload?
|
||||
if (action.payload) {
|
||||
const PAYLOAD = action.payload;
|
||||
|
||||
// Step 1.1: Is the promise implicitly defined?
|
||||
if (isPromise(PAYLOAD)) {
|
||||
promise = PAYLOAD;
|
||||
}
|
||||
|
||||
// Step 1.2: Is the promise explicitly defined?
|
||||
else if (isPromise(PAYLOAD.promise)) {
|
||||
promise = PAYLOAD.promise;
|
||||
data = PAYLOAD.data;
|
||||
}
|
||||
|
||||
// Step 1.3: Is the promise returned by an async function?
|
||||
else if (typeof PAYLOAD === 'function' || typeof PAYLOAD.promise === 'function') {
|
||||
promise = PAYLOAD.promise ? PAYLOAD.promise() : PAYLOAD();
|
||||
data = PAYLOAD.promise ? PAYLOAD.data : undefined;
|
||||
|
||||
// Step 1.3.1: Is the return of action.payload a promise?
|
||||
if (!isPromise(promise)) {
|
||||
// If not, move on to the next middleware.
|
||||
return next({
|
||||
...action,
|
||||
payload: promise,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 1.4: If there's no promise, move on to the next middleware.
|
||||
else {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// Step 1b: If there's no payload, move on to the next middleware.
|
||||
} else {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate and define constants for:
|
||||
* (1) the action type
|
||||
* (2) the action meta
|
||||
*/
|
||||
const TYPE = action.type;
|
||||
const META = action.meta;
|
||||
|
||||
/**
|
||||
* Instantiate and define constants for the action type suffixes.
|
||||
* These are appended to the end of the action type.
|
||||
*/
|
||||
const [PENDING, FULFILLED, REJECTED] = PROMISE_TYPE_SUFFIXES;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import { createStore, applyMiddleware, thunk } from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||
|
||||
describe('Redux thunk', () => {
|
||||
it('should use apply thunk middleware', async () => {
|
||||
const MAX_TODOS = 5;
|
||||
|
||||
function addTodosIfAllowed(todoText) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
if (state.todos.length < MAX_TODOS) {
|
||||
dispatch({ type: 'ADD_TODO', text: todoText });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const todoStore = createStore(
|
||||
(state = { todos: [] }, action) => {
|
||||
if (action.type === 'ADD_TODO') {
|
||||
return { todos: state.todos?.concat(action.text) };
|
||||
}
|
||||
return state;
|
||||
},
|
||||
null,
|
||||
applyMiddleware(thunk)
|
||||
);
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
todoStore.dispatch(addTodosIfAllowed('todo no.' + i));
|
||||
}
|
||||
|
||||
expect(todoStore.getState().todos.length).toBe(5);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,358 @@
|
|||
import horizon, * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import {
|
||||
batch,
|
||||
connect,
|
||||
createStore,
|
||||
Provider,
|
||||
useDispatch,
|
||||
useSelector,
|
||||
useStore,
|
||||
createSelectorHook,
|
||||
createDispatchHook,
|
||||
} from '../../../../libs/horizon/src/horizonx/adapters/redux';
|
||||
import { triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
const BUTTON = 'button';
|
||||
const BUTTON2 = 'button2';
|
||||
const RESULT = 'result';
|
||||
const CONTAINER = 'container';
|
||||
|
||||
function getE(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
describe('Redux/React binding adapter', () => {
|
||||
beforeEach(() => {
|
||||
const container = document.createElement('div');
|
||||
container.id = CONTAINER;
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(getE(CONTAINER));
|
||||
});
|
||||
|
||||
it('Should create provider context', async () => {
|
||||
const reduxStore = createStore((state = 'state', action) => state);
|
||||
|
||||
const Child = () => {
|
||||
const store = useStore();
|
||||
return <div id={RESULT}>{store.getState()}</div>;
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
return (
|
||||
<Provider store={reduxStore}>
|
||||
<Child />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('state');
|
||||
});
|
||||
|
||||
it('Should use dispatch', async () => {
|
||||
const reduxStore = createStore((state = 0, action) => {
|
||||
if (action.type === 'ADD') return state + 1;
|
||||
return state;
|
||||
});
|
||||
|
||||
const Child = () => {
|
||||
const store = useStore();
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT}>{store.getState()}</p>
|
||||
<button
|
||||
id={BUTTON}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'ADD' });
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
return (
|
||||
<Provider store={reduxStore}>
|
||||
<Child />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||
|
||||
expect(reduxStore.getState()).toBe(0);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
});
|
||||
|
||||
expect(reduxStore.getState()).toBe(1);
|
||||
});
|
||||
|
||||
it('Should use selector', async () => {
|
||||
const reduxStore = createStore((state = 0, action) => {
|
||||
if (action.type === 'ADD') return state + 1;
|
||||
return state;
|
||||
});
|
||||
|
||||
const Child = () => {
|
||||
const count = useSelector(state => state);
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT}>{count}</p>
|
||||
<button
|
||||
id={BUTTON}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'ADD' });
|
||||
}}
|
||||
>
|
||||
click
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Wrapper = () => {
|
||||
return (
|
||||
<Provider store={reduxStore}>
|
||||
<Child />
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('0');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
});
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('2');
|
||||
});
|
||||
|
||||
it('Should use connect', async () => {
|
||||
const reduxStore = createStore(
|
||||
(state, action) => {
|
||||
switch (action.type) {
|
||||
case 'INCREMENT':
|
||||
return {
|
||||
...state,
|
||||
value: state.negative ? state.value - action.amount : state.value + action.amount,
|
||||
};
|
||||
case 'TOGGLE':
|
||||
return {
|
||||
...state,
|
||||
negative: !state.negative,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
},
|
||||
{ negative: false, value: 0 }
|
||||
);
|
||||
|
||||
const Child = connect(
|
||||
(state, ownProps) => {
|
||||
// map state to props
|
||||
return { ...state, ...ownProps };
|
||||
},
|
||||
(dispatch, ownProps) => {
|
||||
// map dispatch to props
|
||||
return {
|
||||
increment: () => dispatch({ type: 'INCREMENT', amount: ownProps.amount }),
|
||||
};
|
||||
},
|
||||
(stateProps, dispatchProps, ownProps) => {
|
||||
//merge props
|
||||
return { stateProps, dispatchProps, ownProps };
|
||||
},
|
||||
{}
|
||||
)(props => {
|
||||
const n = props.stateProps.negative;
|
||||
return (
|
||||
<div>
|
||||
<div id={RESULT}>
|
||||
{n ? '-' : '+'}
|
||||
{props.stateProps.value}
|
||||
</div>
|
||||
<button
|
||||
id={BUTTON}
|
||||
onClick={() => {
|
||||
props.dispatchProps.increment();
|
||||
}}
|
||||
>
|
||||
add {props.ownProps.amount}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Wrapper = () => {
|
||||
const [amount, setAmount] = Horizon.useState(5);
|
||||
return (
|
||||
<Provider store={reduxStore}>
|
||||
<Child amount={amount} />
|
||||
<button
|
||||
id={BUTTON2}
|
||||
onClick={() => {
|
||||
setAmount(3);
|
||||
}}
|
||||
>
|
||||
change amount
|
||||
</button>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('+0');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
});
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('+5');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON2);
|
||||
});
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
});
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('+8');
|
||||
});
|
||||
|
||||
it('Should batch dispatches', async () => {
|
||||
const reduxStore = createStore((state = 0, action) => {
|
||||
if (action.type == 'ADD') return state + 1;
|
||||
return state;
|
||||
});
|
||||
|
||||
let renderCounter = 0;
|
||||
|
||||
function Counter() {
|
||||
renderCounter++;
|
||||
|
||||
const value = useSelector(state => state);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p id={RESULT}>{value}</p>
|
||||
<button
|
||||
id={BUTTON}
|
||||
onClick={() => {
|
||||
batch(() => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
dispatch({ type: 'ADD' });
|
||||
}
|
||||
});
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(
|
||||
<Provider store={reduxStore}>
|
||||
<Counter />
|
||||
</Provider>,
|
||||
getE(CONTAINER)
|
||||
);
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('0');
|
||||
expect(renderCounter).toBe(1);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
});
|
||||
|
||||
expect(getE(RESULT).innerHTML).toBe('10');
|
||||
expect(renderCounter).toBe(2);
|
||||
});
|
||||
|
||||
it('Should use multiple contexts', async () => {
|
||||
const counterStore = createStore((state = 0, action) => {
|
||||
if (action.type === 'ADD') return state + 1;
|
||||
return state;
|
||||
});
|
||||
|
||||
const toggleStore = createStore((state = false, action) => {
|
||||
if (action.type === 'TOGGLE') return !state;
|
||||
return state;
|
||||
});
|
||||
|
||||
const counterContext = horizon.createContext();
|
||||
const toggleContext = horizon.createContext();
|
||||
|
||||
function Counter() {
|
||||
const count = createSelectorHook(counterContext)();
|
||||
const dispatch = createDispatchHook(counterContext)();
|
||||
|
||||
return (
|
||||
<button
|
||||
id={BUTTON}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'ADD' });
|
||||
}}
|
||||
>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle() {
|
||||
const check = createSelectorHook(toggleContext)();
|
||||
const dispatch = createDispatchHook(toggleContext)();
|
||||
|
||||
return (
|
||||
<button
|
||||
id={BUTTON2}
|
||||
onClick={() => {
|
||||
dispatch({ type: 'TOGGLE' });
|
||||
}}
|
||||
>
|
||||
{check ? 'true' : 'false'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Wrapper() {
|
||||
return (
|
||||
<div>
|
||||
<Provider store={counterStore} context={counterContext}>
|
||||
<Counter />
|
||||
</Provider>
|
||||
|
||||
<Provider store={toggleStore} context={toggleContext}>
|
||||
<Toggle />
|
||||
</Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Horizon.render(<Wrapper />, getE(CONTAINER));
|
||||
|
||||
expect(getE(BUTTON).innerHTML).toBe('0');
|
||||
expect(getE(BUTTON2).innerHTML).toBe('false');
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON);
|
||||
triggerClickEvent(getE(CONTAINER), BUTTON2);
|
||||
});
|
||||
|
||||
expect(getE(BUTTON).innerHTML).toBe('1');
|
||||
expect(getE(BUTTON2).innerHTML).toBe('true');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { Text } from '../../jest/commonComponents';
|
||||
|
||||
describe('测试 Class VNode 清除时,对引用清除', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
let globalState = {
|
||||
name: 'bing dun dun',
|
||||
isWin: true,
|
||||
isShow: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: globalState,
|
||||
actions: {
|
||||
setWin: (state, val) => {
|
||||
state.isWin = val;
|
||||
},
|
||||
hide: state => {
|
||||
state.isShow = false;
|
||||
},
|
||||
updateName: (state, val) => {
|
||||
state.name = val;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
it('test observer.clearByNode', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
// Do not modify the store data in the render method. Otherwise, an infinite loop may occur.
|
||||
this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
expect(() => {
|
||||
Horizon.render(<Child />, container);
|
||||
}).toThrow(
|
||||
'The number of updates exceeds the upper limit 50.\n' +
|
||||
' A component maybe repeatedly invokes setState on componentWillUpdate or componentDidUpdate.'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,220 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('在Class组件中,测试store中的Array', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = [
|
||||
{ name: 'p1', age: 1 },
|
||||
{ name: 'p2', age: 2 },
|
||||
];
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.push(person);
|
||||
},
|
||||
delOnePerson: state => {
|
||||
state.persons.pop();
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons = null;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
addOnePerson = () => {
|
||||
this.userStore.addOnePerson(newPerson);
|
||||
};
|
||||
|
||||
delOnePerson = () => {
|
||||
this.userStore.delOnePerson();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={this.addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={this.delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
it('测试Array方法: push()、pop()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${this.userStore.persons.length}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
|
||||
// 在Array中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 3');
|
||||
|
||||
// 在Array中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: 2');
|
||||
});
|
||||
|
||||
it('测试Array方法: entries()、push()、shift()、unshift、直接赋值', () => {
|
||||
let globalStore = null;
|
||||
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
globalStore = this.userStore;
|
||||
}
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
const entries = this.userStore.$state.persons?.entries();
|
||||
if (entries) {
|
||||
for (const entry of entries) {
|
||||
nameList.push(entry[1].name);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// push
|
||||
globalStore.$state.persons.push(newPerson);
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
|
||||
|
||||
// 重新赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
|
||||
|
||||
// unshift
|
||||
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
|
||||
|
||||
// 重新赋值 null
|
||||
globalStore.$state.persons = null;
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
|
||||
// 重新赋值 [{ name: 'p1', age: 1 }]
|
||||
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
|
||||
});
|
||||
|
||||
it('测试Array方法: forEach()', () => {
|
||||
let globalStore = null;
|
||||
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
globalStore = this.userStore;
|
||||
}
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
this.userStore.$state.persons?.forEach(per => {
|
||||
nameList.push(per.name);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// push
|
||||
globalStore.$state.persons.push(newPerson);
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// shift
|
||||
globalStore.$state.persons.shift({ name: 'p0', age: 0 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3');
|
||||
|
||||
// 赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p4', age: 4 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p4');
|
||||
|
||||
// 重新赋值[2]
|
||||
globalStore.$state.persons[2] = { name: 'p5', age: 5 };
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p2 p3 p5');
|
||||
|
||||
// unshift
|
||||
globalStore.$state.persons.unshift({ name: 'p1', age: 1 });
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3 p5');
|
||||
|
||||
// 重新赋值 null
|
||||
globalStore.$state.persons = null;
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
|
||||
// 重新赋值 [{ name: 'p1', age: 1 }]
|
||||
globalStore.$state.persons = [{ name: 'p1', age: 1 }];
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,340 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { App, Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
|
||||
describe('在Class组件中,测试store中的Map', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const persons = new Map([
|
||||
['p1', 1],
|
||||
['p2', 2],
|
||||
]);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: {
|
||||
type: 'bing dun dun',
|
||||
persons: persons,
|
||||
},
|
||||
actions: {
|
||||
addOnePerson: (state, person) => {
|
||||
state.persons.set(person.name, person.age);
|
||||
},
|
||||
delOnePerson: (state, person) => {
|
||||
state.persons.delete(person.name);
|
||||
},
|
||||
clearPersons: state => {
|
||||
state.persons.clear();
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
const newPerson = { name: 'p3', age: 3 };
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
addOnePerson = () => {
|
||||
this.userStore.addOnePerson(newPerson);
|
||||
};
|
||||
delOnePerson = () => {
|
||||
this.userStore.delOnePerson(newPerson);
|
||||
};
|
||||
clearPersons = () => {
|
||||
this.userStore.clearPersons();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'addBtn'} onClick={this.addOnePerson}>
|
||||
add person
|
||||
</button>
|
||||
<button id={'delBtn'} onClick={this.delOnePerson}>
|
||||
delete person
|
||||
</button>
|
||||
<button id={'clearBtn'} onClick={this.clearPersons}>
|
||||
clear persons
|
||||
</button>
|
||||
<div>{this.props.children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
it('测试Map方法: set()、delete()、clear()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Text id={'size'} text={`persons number: ${this.userStore.$state.persons.size}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#size').innerHTML).toBe('persons number: 0');
|
||||
});
|
||||
|
||||
it('测试Map方法: keys()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
const keys = this.userStore.$state.persons.keys();
|
||||
for (const key of keys) {
|
||||
nameList.push(key);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: values()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
const ageList = [];
|
||||
const values = this.userStore.$state.persons.values();
|
||||
for (const val of values) {
|
||||
ageList.push(val);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'ageList'} text={`age list: ${ageList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2 3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: 1 2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#ageList').innerHTML).toBe('age list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: entries()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
const entries = this.userStore.$state.persons.entries();
|
||||
for (const entry of entries) {
|
||||
nameList.push(entry[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: forEach()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
this.userStore.$state.persons.forEach((val, key) => {
|
||||
nameList.push(key);
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
|
||||
it('测试Map方法: has()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Text id={'hasPerson'} text={`has new person: ${this.userStore.$state.persons.has(newPerson.name)}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: false');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#hasPerson').innerHTML).toBe('has new person: true');
|
||||
});
|
||||
|
||||
it('测试Map方法: for of()', () => {
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
const nameList = [];
|
||||
for (const per of this.userStore.$state.persons) {
|
||||
nameList.push(per[0]);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'nameList'} text={`name list: ${nameList.join(' ')}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App parent={Parent} child={Child} />, container);
|
||||
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
// 在Map中增加一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'addBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2 p3');
|
||||
|
||||
// 在Map中删除一个对象
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'delBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: p1 p2');
|
||||
|
||||
// clear Map
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'clearBtn');
|
||||
});
|
||||
expect(container.querySelector('#nameList').innerHTML).toBe('name list: ');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,119 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
|
||||
|
||||
describe('测试 Class VNode 清除时,对引用清除', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
let globalState = {
|
||||
name: 'bing dun dun',
|
||||
isWin: true,
|
||||
isShow: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: globalState,
|
||||
actions: {
|
||||
setWin: (state, val) => {
|
||||
state.isWin = val;
|
||||
},
|
||||
hide: state => {
|
||||
state.isShow = false;
|
||||
},
|
||||
updateName: (state, val) => {
|
||||
state.name = val;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
it('test observer.clearByNode', () => {
|
||||
class App extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'hideBtn'} onClick={this.userStore.hide}>
|
||||
toggle
|
||||
</button>
|
||||
{this.userStore.isShow && <Parent />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
setWin = () => {
|
||||
this.userStore.setWin(!this.userStore.isWin);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'toggleBtn'} onClick={this.setWin}>
|
||||
toggle
|
||||
</button>
|
||||
{this.userStore.isWin && <Child />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
// this.userStore.updateName(this.userStore.name === 'bing dun dun' ? 'huo dun dun' : 'bing dun dun');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
// Parent and Child hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'toggleBtn');
|
||||
});
|
||||
// Parent hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'toggleBtn');
|
||||
});
|
||||
// Parent and Child hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'hideBtn');
|
||||
});
|
||||
// no component hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,114 @@
|
|||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { clearStore, createStore, useStore } from '../../../../libs/horizon/src/horizonx/store/StoreHandler';
|
||||
import { Text, triggerClickEvent } from '../../jest/commonComponents';
|
||||
import { getObserver } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
|
||||
|
||||
describe('测试VNode清除时,对引用清除', () => {
|
||||
const { unmountComponentAtNode } = Horizon;
|
||||
let container = null;
|
||||
let globalState = {
|
||||
name: 'bing dun dun',
|
||||
isWin: true,
|
||||
isShow: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建一个 DOM 元素作为渲染目标
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
createStore({
|
||||
id: 'user',
|
||||
state: globalState,
|
||||
actions: {
|
||||
setWin: (state, val) => {
|
||||
state.isWin = val;
|
||||
},
|
||||
hide: state => {
|
||||
state.isShow = false;
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// 退出时进行清理
|
||||
unmountComponentAtNode(container);
|
||||
container.remove();
|
||||
container = null;
|
||||
|
||||
clearStore('user');
|
||||
});
|
||||
|
||||
it('test observer.clearByNode', () => {
|
||||
class App extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'hideBtn'} onClick={this.userStore.hide}>
|
||||
toggle
|
||||
</button>
|
||||
{this.userStore.isShow && <Parent />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
setWin = () => {
|
||||
this.userStore.setWin(!this.userStore.isWin);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<button id={'toggleBtn'} onClick={this.setWin}>
|
||||
toggle
|
||||
</button>
|
||||
{this.userStore.isWin && <Child />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Child extends Horizon.Component {
|
||||
userStore = useStore('user');
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Text id={'name'} text={`name: ${this.userStore.name}`} />
|
||||
<Text id={'isWin'} text={`isWin: ${this.userStore.isWin}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Horizon.render(<App />, container);
|
||||
|
||||
// Parent and Child hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'toggleBtn');
|
||||
});
|
||||
// Parent hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(1);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'toggleBtn');
|
||||
});
|
||||
// Parent and Child hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin').size).toBe(2);
|
||||
|
||||
Horizon.act(() => {
|
||||
triggerClickEvent(container, 'hideBtn');
|
||||
});
|
||||
// no component hold the isWin key
|
||||
expect(getObserver(globalState).keyVNodes.get('isWin')).toBe(undefined);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import { createProxy } from '../../../../libs/horizon/src/horizonx/proxy/ProxyHandler';
|
||||
|
||||
describe('Proxy', () => {
|
||||
const arr = [];
|
||||
|
||||
it('Should not double wrap proxies', async () => {
|
||||
const proxy1 = createProxy(arr);
|
||||
|
||||
const proxy2 = createProxy(proxy1);
|
||||
|
||||
expect(proxy1 === proxy2).toBe(true);
|
||||
});
|
||||
|
||||
it('Should re-use existing proxy of same object', async () => {
|
||||
const proxy1 = createProxy(arr);
|
||||
|
||||
const proxy2 = createProxy(arr);
|
||||
|
||||
expect(proxy1 === proxy2).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,9 +1,8 @@
|
|||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||
import { getLogUtils } from './testUtils';
|
||||
|
||||
export const App = (props) => {
|
||||
export const App = props => {
|
||||
const Parent = props.parent;
|
||||
const Child = props.child;
|
||||
|
||||
|
@ -16,8 +15,15 @@ export const App = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const Text = (props) => {
|
||||
const LogUtils =getLogUtils();
|
||||
export const Text = props => {
|
||||
const LogUtils = getLogUtils();
|
||||
LogUtils.log(props.text);
|
||||
return <p id={props.id}>{props.text}</p>;
|
||||
};
|
||||
|
||||
export function triggerClickEvent(container, id) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
});
|
||||
container.querySelector(`#${id}`).dispatchEvent(event);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { allDelegatedNativeEvents } from '../../../libs/horizon/src/event/EventCollection';
|
||||
import { allDelegatedNativeEvents } from '@cloudsop/horizon/src/event/EventHub';
|
||||
//import * as LogUtils from './logUtils';
|
||||
|
||||
export const stopBubbleOrCapture = (e, value) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import copy from './copy-plugin';
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
@ -11,6 +12,14 @@ const extensions = ['.js', '.ts'];
|
|||
const libDir = path.join(__dirname, '../../libs/horizon');
|
||||
const rootDir = path.join(__dirname, '../..');
|
||||
const outDir = path.join(rootDir, 'build', 'horizon');
|
||||
|
||||
if (!fs.existsSync(path.join(rootDir, 'build'))) {
|
||||
fs.mkdirSync(path.join(rootDir, 'build'));
|
||||
}
|
||||
if (!fs.existsSync(outDir)) {
|
||||
fs.mkdirSync(outDir);
|
||||
}
|
||||
|
||||
const outputResolve = (...p) => path.resolve(outDir, ...p);
|
||||
|
||||
function genConfig(mode) {
|
||||
|
@ -52,7 +61,7 @@ function genConfig(mode) {
|
|||
mode === 'production' && terser(),
|
||||
copy([
|
||||
{
|
||||
from: path.join(libDir, 'index.js'),
|
||||
from: path.join(libDir, '/npm/index.js'),
|
||||
to: path.join(outDir, 'index.js'),
|
||||
},
|
||||
{
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue