Match-id-c8f42259603ae600fa183076f7c3db48d32b9065

This commit is contained in:
* 2022-07-11 11:20:21 +08:00 committed by *
commit d087bf644f
87 changed files with 5440 additions and 614 deletions

7
CHANGELOG.md Normal file
View File

@ -0,0 +1,7 @@
## 0.0.8 (2022-07-08)
### Features
- 增加HorizonX提供状态管理能力
### Bug Fixes
- **core**: 修复局部更新场景下context计算错误
### Code Refactoring
- 重构事件机制,取消全量挂载事件,改为按需懒挂载

27
fixtures/antd/.babelrc Normal file
View File

@ -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"
}
]
]
}

3
fixtures/antd/README.md Normal file
View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

29
fixtures/antd/index.jsx Normal file
View File

@ -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'));

View File

@ -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"
}
}

View File

@ -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;

View File

@ -27,10 +27,12 @@ import {
useState, useState,
useDebugValue useDebugValue
} from './src/renderer/hooks/HookExternal'; } 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 { callRenderQueueImmediate } from './src/renderer/taskExecutor/RenderQueue';
import { runAsyncEffects } from './src/renderer/submit/HookEffectHandler'; 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后面的代码是在刷新完成后才执行。 // act用于测试作用是如果fun触发了刷新包含了异步刷新可以保证在act后面的代码是在刷新完成后才执行。
const act = fun => { const act = fun => {
@ -79,8 +81,10 @@ const Horizon = {
findDOMNode, findDOMNode,
unmountComponentAtNode, unmountComponentAtNode,
act, act,
_launchUpdateFromVNode, createStore,
_getProcessingVNode, useStore,
clearStore,
reduxAdapter,
}; };
export const version = __VERSION__; export const version = __VERSION__;
@ -116,9 +120,11 @@ export {
findDOMNode, findDOMNode,
unmountComponentAtNode, unmountComponentAtNode,
act, act,
// 暂时给HorizonX使用 // 状态管理器HorizonX接口
_launchUpdateFromVNode, createStore,
_getProcessingVNode, useStore,
clearStore,
reduxAdapter,
}; };
export default Horizon; export default Horizon;

View File

@ -4,7 +4,7 @@
"keywords": [ "keywords": [
"horizon" "horizon"
], ],
"version": "0.0.7", "version": "0.0.8",
"homepage": "", "homepage": "",
"bugs": "", "bugs": "",
"main": "index.js", "main": "index.js",

View File

@ -22,9 +22,6 @@ function createRoot(children: any, container: Container, callback?: Callback) {
const treeRoot = createTreeRootVNode(container); const treeRoot = createTreeRootVNode(container);
container._treeRoot = treeRoot; container._treeRoot = treeRoot;
// 根节点挂接全量事件
listenDelegatedEvents(container as Element);
// 执行回调 // 执行回调
if (typeof callback === 'function') { if (typeof callback === 'function') {
const cb = callback; const cb = callback;

View File

@ -49,23 +49,17 @@ export function getVNode(dom: Node|Container): VNode | null {
// 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode // 用 DOM 对象,来寻找其对应或者说是最近父级的 vNode
export function getNearestVNode(dom: Node): null | VNode { export function getNearestVNode(dom: Node): null | VNode {
let vNode = dom[INTERNAL_VNODE]; let domNode: Node | null = dom;
if (vNode) { // 如果是已经被框架标记过的 DOM 节点,那么直接返回其 VNode 实例 // 寻找当前节点及其所有祖先节点是否有标记VNODE
while (domNode) {
const vNode = domNode[INTERNAL_VNODE];
if (vNode) {
return vNode; return vNode;
} }
domNode = domNode.parentNode;
}
// 下面处理的是为被框架标记过的 DOM 节点,向上找其父节点是否被框架标记过 return null;
let parentDom = dom.parentNode;
let nearVNode = null;
while (parentDom) {
vNode = parentDom[INTERNAL_VNODE];
if (vNode) {
nearVNode = vNode;
break;
}
parentDom = parentDom.parentNode;
}
return nearVNode;
} }
// 获取 vNode 上的属性相关信息 // 获取 vNode 上的属性相关信息

View File

@ -26,7 +26,7 @@ import { watchValueChange } from './valueHandler/ValueChangeHandler';
import { DomComponent, DomText } from '../renderer/vnode/VNodeTags'; import { DomComponent, DomText } from '../renderer/vnode/VNodeTags';
import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp'; import { updateCommonProp } from './DOMPropertiesHandler/UpdateCommonProp';
export type Props = { export type Props = Record<string, any> & {
autoFocus?: boolean; autoFocus?: boolean;
children?: any; children?: any;
dangerouslySetInnerHTML?: any; dangerouslySetInnerHTML?: any;

View File

@ -1,20 +1,12 @@
import { import { allDelegatedHorizonEvents } from '../../event/EventHub';
allDelegatedHorizonEvents,
} from '../../event/EventCollection';
import { updateCommonProp } from './UpdateCommonProp'; import { updateCommonProp } from './UpdateCommonProp';
import { setStyles } from './StyleHandler'; import { setStyles } from './StyleHandler';
import { import { lazyDelegateOnRoot, listenNonDelegatedEvent } from '../../event/EventBinding';
listenNonDelegatedEvent
} from '../../event/EventBinding';
import { isEventProp } from '../validators/ValidateProps'; import { isEventProp } from '../validators/ValidateProps';
import { getCurrentRoot } from '../../renderer/TreeBuilder';
// 初始化DOM属性和更新 DOM 属性 // 初始化DOM属性和更新 DOM 属性
export function setDomProps( export function setDomProps(dom: Element, props: Object, isNativeTag: boolean, isInit: boolean): void {
dom: Element,
props: Object,
isNativeTag: boolean,
isInit: boolean,
): void {
const keysOfProps = Object.keys(props); const keysOfProps = Object.keys(props);
let propName; let propName;
let propVal; let propVal;
@ -27,10 +19,14 @@ export function setDomProps(
setStyles(dom, propVal); setStyles(dom, propVal);
} else if (isEventProp(propName)) { } else if (isEventProp(propName)) {
// 事件监听属性处理 // 事件监听属性处理
const currentRoot = getCurrentRoot();
if (!allDelegatedHorizonEvents.has(propName)) { if (!allDelegatedHorizonEvents.has(propName)) {
listenNonDelegatedEvent(propName, dom, propVal); 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; const type = typeof propVal;
if (type === 'string' || type === 'number') { if (type === 'string' || type === 'number') {
dom.textContent = propVal; dom.textContent = propVal;
@ -44,10 +40,7 @@ export function setDomProps(
} }
// 找出两个 DOM 属性的差别,生成需要更新的属性集合 // 找出两个 DOM 属性的差别,生成需要更新的属性集合
export function compareProps( export function compareProps(oldProps: Object, newProps: Object): Object {
oldProps: Object,
newProps: Object,
): Object {
let updatesForStyle = {}; let updatesForStyle = {};
const toUpdateProps = {}; const toUpdateProps = {};
const keysOfOldProps = Object.keys(oldProps); const keysOfOldProps = Object.keys(oldProps);
@ -107,7 +100,8 @@ export function compareProps(
} }
if (propName === 'style') { if (propName === 'style') {
if (oldPropValue) { // 之前 style 属性有设置非空值 if (oldPropValue) {
// 之前 style 属性有设置非空值
// 原来有这个 style但现在没这个 style 了 // 原来有这个 style但现在没这个 style 了
oldStyleProps = Object.keys(oldPropValue); oldStyleProps = Object.keys(oldPropValue);
for (let j = 0; j < oldStyleProps.length; j++) { for (let j = 0; j < oldStyleProps.length; j++) {
@ -125,7 +119,8 @@ export function compareProps(
updatesForStyle[styleProp] = newPropValue[styleProp]; updatesForStyle[styleProp] = newPropValue[styleProp];
} }
} }
} else { // 之前未设置 style 属性或者设置了空值 } else {
// 之前未设置 style 属性或者设置了空值
if (Object.keys(updatesForStyle).length === 0) { if (Object.keys(updatesForStyle).length === 0) {
toUpdateProps[propName] = null; toUpdateProps[propName] = null;
} }
@ -144,8 +139,11 @@ export function compareProps(
toUpdateProps[propName] = String(newPropValue); toUpdateProps[propName] = String(newPropValue);
} }
} else if (isEventProp(propName)) { } else if (isEventProp(propName)) {
const currentRoot = getCurrentRoot();
if (!allDelegatedHorizonEvents.has(propName)) { if (!allDelegatedHorizonEvents.has(propName)) {
toUpdateProps[propName] = newPropValue; toUpdateProps[propName] = newPropValue;
} else if (currentRoot && !currentRoot.delegatedEvents.has(propName)) {
lazyDelegateOnRoot(currentRoot, propName);
} }
} else { } else {
toUpdateProps[propName] = newPropValue; toUpdateProps[propName] = newPropValue;

View File

@ -1,12 +1,12 @@
function isNeedUnitCSS(propName: string) { function isNeedUnitCSS(styleName: string) {
return !(noUnitCSS.includes(propName) return !(noUnitCSS.includes(styleName)
|| propName.startsWith('borderImage') || styleName.startsWith('borderImage')
|| propName.startsWith('flex') || styleName.startsWith('flex')
|| propName.startsWith('gridRow') || styleName.startsWith('gridRow')
|| propName.startsWith('gridColumn') || styleName.startsWith('gridColumn')
|| propName.startsWith('stroke') || styleName.startsWith('stroke')
|| propName.startsWith('box') || styleName.startsWith('box')
|| propName.endsWith('Opacity')); || styleName.endsWith('Opacity'));
} }
/** /**
@ -38,9 +38,7 @@ export function setStyles(dom, styles) {
Object.keys(styles).forEach((name) => { Object.keys(styles).forEach((name) => {
const styleVal = styles[name]; const styleVal = styles[name];
const validStyleValue = adjustStyleValue(name, styleVal); style[name] = adjustStyleValue(name, styleVal);
style[name] = validStyleValue;
}); });
} }

View File

@ -56,6 +56,10 @@ export function getDomTag(dom) {
return dom.nodeName.toLowerCase(); return dom.nodeName.toLowerCase();
} }
export function isInputElement(dom: Element): dom is HTMLInputElement {
return getDomTag(dom) === 'input';
}
const types = ['button', 'input', 'select', 'textarea']; const types = ['button', 'input', 'select', 'textarea'];
// button、input、select、textarea、如果有 autoFocus 属性需要focus // button、input、select、textarea、如果有 autoFocus 属性需要focus

View File

@ -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; return !tagName.includes('-') && props.is === undefined;
} }

View File

@ -1,20 +1,21 @@
import {updateCommonProp} from '../DOMPropertiesHandler/UpdateCommonProp'; import { updateCommonProp } from '../DOMPropertiesHandler/UpdateCommonProp';
import {getVNodeProps} from '../DOMInternalKeys'; import { IProperty } from '../utils/Interface';
import {IProperty} from '../utils/Interface'; import { isInputElement } from '../utils/Common';
import {isInputValueChanged} from './ValueChangeHandler'; import { getVNodeProps } from '../DOMInternalKeys';
import { updateInputValueIfChanged } from './ValueChangeHandler';
function getInitValue(dom: HTMLInputElement, properties: IProperty) { function getInitValue(dom: HTMLInputElement, properties: IProperty) {
const {value, defaultValue, checked, defaultChecked} = properties; const { value, defaultValue, checked, defaultChecked } = properties;
const defaultValueStr = defaultValue != null ? defaultValue : ''; const defaultValueStr = defaultValue != null ? defaultValue : '';
const initValue = value != null ? value : defaultValueStr; const initValue = value != null ? value : defaultValueStr;
const initChecked = checked != null ? checked : defaultChecked; const initChecked = checked != null ? checked : defaultChecked;
return {initValue, initChecked}; return { initValue, initChecked };
} }
export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) { export function getInputPropsWithoutValue(dom: HTMLInputElement, properties: IProperty) {
// checked属于必填属性无法置 // checked属于必填属性无法置
let {checked} = properties; let {checked} = properties;
if (checked == null) { if (checked == null) {
checked = getInitValue(dom, properties).initChecked; checked = getInitValue(dom, properties).initChecked;
@ -59,30 +60,26 @@ export function setInitInputValue(dom: HTMLInputElement, properties: IProperty)
dom.defaultChecked = Boolean(initChecked); dom.defaultChecked = Boolean(initChecked);
} }
export function resetInputValue(dom: HTMLInputElement, properties: IProperty) { // 找出同一form内name相同的Radio更新它们Handler的Value
const {name, type} = properties; export function syncRadiosHandler(targetRadio: Element) {
// 如果是 radio先更新相同 name 的 radio if (isInputElement(targetRadio)) {
const props = getVNodeProps(targetRadio);
if (props) {
const { name, type } = props;
if (type === 'radio' && name != null) { if (type === 'radio' && name != null) {
const radioList = document.querySelectorAll(`input[type="radio"][name="${name}"]`); const radioList = document.querySelectorAll<HTMLInputElement>(`input[type="radio"][name="${name}"]`);
for (let i = 0; i < radioList.length; i++) { for (let i = 0; i < radioList.length; i++) {
const radio = radioList[i]; const radio = radioList[i];
if (radio === dom) { if (radio === targetRadio) {
continue; continue;
} }
// @ts-ignore if (radio.form != null && targetRadio.form != null && radio.form !== targetRadio.form) {
if (radio.form !== dom.form) {
continue; continue;
} }
// @ts-ignore updateInputValueIfChanged(radio);
const nonHorizonRadioProps = getVNodeProps(radio); }
}
isInputValueChanged(radio);
// @ts-ignore
updateInputValue(radio, nonHorizonRadioProps);
} }
} else {
updateInputValue(dom, properties);
} }
} }

View File

@ -43,7 +43,7 @@ export function getSelectPropsWithoutValue(dom: HorizonSelect, properties: Objec
return { return {
...properties, ...properties,
value: undefined, value: undefined,
} };
} }
export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) { export function updateSelectValue(dom: HorizonSelect, properties: IProperty, isInit: boolean = false) {

View File

@ -54,7 +54,7 @@ export function watchValueChange(dom) {
} }
} }
export function isInputValueChanged(dom) { export function updateInputValueIfChanged(dom) {
const handler = dom[HANDLER_KEY]; const handler = dom[HANDLER_KEY];
if (!handler) { if (!handler) {
return true; return true;

View File

@ -8,7 +8,6 @@ import {
getInputPropsWithoutValue, getInputPropsWithoutValue,
setInitInputValue, setInitInputValue,
updateInputValue, updateInputValue,
resetInputValue,
} from './InputValueHandler'; } from './InputValueHandler';
import { import {
getOptionPropsWithoutValue, getOptionPropsWithoutValue,
@ -21,7 +20,6 @@ import {
getTextareaPropsWithoutValue, getTextareaPropsWithoutValue,
updateTextareaValue, updateTextareaValue,
} from './TextareaValueHandler'; } from './TextareaValueHandler';
import {getDomTag} from "../utils/Common";
// 获取元素除了被代理的值以外的属性 // 获取元素除了被代理的值以外的属性
function getPropsWithoutValue(type: string, dom: HorizonDom, properties: IProperty) { 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 { export {
getPropsWithoutValue, getPropsWithoutValue,
setInitValue, setInitValue,
updateValue, updateValue,
resetValue,
}; };

View File

@ -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;
}

View File

@ -1,52 +1,41 @@
/** /**
* *
*/ */
import {allDelegatedNativeEvents} from './EventCollection';
import {isDocument} from '../dom/utils/Common';
import { import {
getNearestVNode, allDelegatedHorizonEvents,
getNonDelegatedListenerMap, allDelegatedNativeEvents,
} from '../dom/DOMInternalKeys'; } from './EventHub';
import {runDiscreteUpdates} from '../renderer/TreeBuilder'; import { isDocument } from '../dom/utils/Common';
import {isMounted} from '../renderer/vnode/VNodeUtils'; import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
import {SuspenseComponent} from '../renderer/vnode/VNodeTags'; import { runDiscreteUpdates } from '../renderer/TreeBuilder';
import {handleEventMain} from './HorizonEventMain'; import { handleEventMain } from './HorizonEventMain';
import {decorateNativeEvent} from './customEvents/EventFactory'; 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( function triggerDelegatedEvent(
nativeEvtName: string, nativeEvtName: string,
isCapture: boolean, isCapture: boolean,
targetDom: EventTarget, targetDom: EventTarget,
nativeEvent, // 事件对象event nativeEvent // 事件对象event
) { ) {
// 执行之前的调度事件 // 执行之前的调度事件
runDiscreteUpdates(); runDiscreteUpdates();
const nativeEventTarget = nativeEvent.target || nativeEvent.srcElement; 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); handleEventMain(nativeEvtName, isCapture, nativeEvent, targetVNode, targetDom);
} }
// 监听委托事件 // 监听委托事件
function listenToNativeEvent( function listenToNativeEvent(nativeEvtName: string, delegatedElement: Element, isCapture: boolean): void {
nativeEvtName: string,
delegatedElement: Element,
isCapture: boolean,
): void {
let dom: Element | Document = delegatedElement; let dom: Element | Document = delegatedElement;
// document层次可能触发selectionchange事件为了捕获这类事件selectionchange事件绑定在document节点上 // document层次可能触发selectionchange事件为了捕获这类事件selectionchange事件绑定在document节点上
if (nativeEvtName === 'selectionchange' && !isDocument(delegatedElement)) { 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事件名 // 通过horizon事件名获取到native事件名
function getNativeEvtName(horizonEventName, capture) { function getNativeEvtName(horizonEventName, capture) {
let nativeName; let nativeName;
@ -104,11 +109,7 @@ function getWrapperListener(horizonEventName, nativeEvtName, targetElement, list
} }
// 非委托事件单独监听到各自dom节点 // 非委托事件单独监听到各自dom节点
export function listenNonDelegatedEvent( export function listenNonDelegatedEvent(horizonEventName: string, domElement: Element, listener): void {
horizonEventName: string,
domElement: Element,
listener,
): void {
const isCapture = isCaptureEvent(horizonEventName); const isCapture = isCaptureEvent(horizonEventName);
const nativeEvtName = getNativeEvtName(horizonEventName, isCapture); const nativeEvtName = getNativeEvtName(horizonEventName, isCapture);

View File

@ -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);
});
});

View File

@ -1,3 +1,7 @@
// 需要委托的horizon事件和原生事件对应关系
export const allDelegatedHorizonEvents = new Map();
// 所有委托的原生事件集合
export const allDelegatedNativeEvents = new Set();
// Horizon事件和原生事件对应关系 // Horizon事件和原生事件对应关系
export const horizonEventToNativeMap = new Map([ export const horizonEventToNativeMap = new Map([
@ -28,16 +32,14 @@ export const horizonEventToNativeMap = new Map([
['onCompositionStart', ['compositionstart']], ['onCompositionStart', ['compositionstart']],
['onCompositionUpdate', ['compositionupdate']], ['onCompositionUpdate', ['compositionupdate']],
['onChange', ['change', 'click', 'focusout', 'input']], ['onChange', ['change', 'click', 'focusout', 'input']],
['onSelect', ['focusout', 'contextmenu', 'dragend', 'focusin', ['onSelect', ['select']],
'keydown', 'keyup', 'mousedown', 'mouseup', 'selectionchange']],
['onAnimationEnd', ['animationend']], ['onAnimationEnd', ['animationend']],
['onAnimationIteration', ['animationiteration']], ['onAnimationIteration', ['animationiteration']],
['onAnimationStart', ['animationstart']], ['onAnimationStart', ['animationstart']],
['onTransitionEnd', ['transitionend']] ['onTransitionEnd', ['transitionend']],
]); ]);
export const NativeEventToHorizonMap = {
export const CommonEventToHorizonMap = {
click: 'click', click: 'click',
dblclick: 'doubleClick', dblclick: 'doubleClick',
contextmenu: 'contextMenu', contextmenu: 'contextMenu',
@ -45,6 +47,7 @@ export const CommonEventToHorizonMap = {
focusin: 'focus', focusin: 'focus',
focusout: 'blur', focusout: 'blur',
input: 'input', input: 'input',
select: 'select',
keydown: 'keyDown', keydown: 'keyDown',
keypress: 'keyPress', keypress: 'keyPress',
keyup: 'keyUp', keyup: 'keyUp',
@ -69,11 +72,22 @@ export const CommonEventToHorizonMap = {
compositionend: 'compositionEnd', compositionend: 'compositionEnd',
compositionupdate: 'compositionUpdate', compositionupdate: 'compositionUpdate',
}; };
export const CHAR_CODE_SPACE = 32; export const CHAR_CODE_SPACE = 32;
export const EVENT_TYPE_BUBBLE = 'Bubble'; export const EVENT_TYPE_BUBBLE = 'Bubble';
export const EVENT_TYPE_CAPTURE = 'Capture'; export const EVENT_TYPE_CAPTURE = 'Capture';
export const EVENT_TYPE_ALL = 'All'; 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)}`;
}

View File

@ -1,23 +1,75 @@
import type { AnyNativeEvent } from './Types'; import { AnyNativeEvent, ListenerUnitList } from './Types';
import type { VNode } from '../renderer/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 { import {
CommonEventToHorizonMap, EVENT_TYPE_ALL,
horizonEventToNativeMap,
EVENT_TYPE_BUBBLE, EVENT_TYPE_BUBBLE,
EVENT_TYPE_CAPTURE, EVENT_TYPE_CAPTURE,
} from './const'; horizonEventToNativeMap,
import { getListeners as getChangeListeners } from './simulatedEvtHandler/ChangeEventHandler'; transformToHorizonEvent,
import { getListeners as getSelectionListeners } from './simulatedEvtHandler/SelectionEventHandler'; } from './EventHub';
import { import { getDomTag } from '../dom/utils/Common';
setPropertyWritable, import { updateInputValueIfChanged } from '../dom/valueHandler/ValueChangeHandler';
} from './utils'; import { getDom } from '../dom/DOMInternalKeys';
import { decorateNativeEvent } from './customEvents/EventFactory';
import { getListenersFromTree } from './ListenerGetter'; // web规范鼠标右键key值
import { shouldUpdateValue, updateControlledValue } from './ControlledValueUpdater'; const RIGHT_MOUSE_BUTTON = 2;
import { asyncUpdates, runDiscreteUpdates } from '../renderer/Renderer';
import { getExactNode } from '../renderer/vnode/VNodeUtils'; // 返回是否需要触发change事件标记
import {ListenerUnitList} from './Types'; // | 元素 | 事件 | 需要值变更 |
// | --- | --- | --------------- |
// | <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( function getCommonListeners(
@ -27,15 +79,14 @@ function getCommonListeners(
target: null | EventTarget, target: null | EventTarget,
isCapture: boolean, isCapture: boolean,
): ListenerUnitList { ): ListenerUnitList {
const name = CommonEventToHorizonMap[nativeEvtName]; const horizonEvtName = transformToHorizonEvent(nativeEvtName);
const horizonEvtName = !name ? '' : `on${name[0].toUpperCase()}${name.slice(1)}`; // 例dragEnd -> onDragEnd
if (!horizonEvtName) { if (!horizonEvtName) {
return []; return [];
} }
// 鼠标点击右键 // 鼠标点击右键
if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === 2) { if (nativeEvent instanceof MouseEvent && nativeEvtName === 'click' && nativeEvent.button === RIGHT_MOUSE_BUTTON) {
return []; return [];
} }
@ -71,13 +122,16 @@ function processListeners(listenerList: ListenerUnitList): void {
}); });
} }
function getProcessListeners( // 触发可以被执行的horizon事件监听
function triggerHorizonEvents(
nativeEvtName: string, nativeEvtName: string,
vNode: VNode | null, isCapture: boolean,
nativeEvent: AnyNativeEvent, nativeEvent: AnyNativeEvent,
target, vNode: VNode | null,
isCapture: boolean ) {
): ListenerUnitList { const target = nativeEvent.target || nativeEvent.srcElement;
let hasTriggeredChangeEvent = false;
// 触发普通委托事件 // 触发普通委托事件
let listenerList: ListenerUnitList = getCommonListeners( let listenerList: ListenerUnitList = getCommonListeners(
nativeEvtName, nativeEvtName,
@ -88,42 +142,22 @@ function getProcessListeners(
); );
// 触发特殊handler委托事件 // 触发特殊handler委托事件
if (!isCapture) { if (!isCapture && horizonEventToNativeMap.get('onChange')!.includes(nativeEvtName)) {
if (horizonEventToNativeMap.get('onChange').includes(nativeEvtName)) { const changeListeners = getChangeListeners(
listenerList = listenerList.concat(getChangeListeners(
nativeEvtName, nativeEvtName,
nativeEvent, nativeEvent,
vNode, vNode,
target, );
)); if (changeListeners.length) {
} hasTriggeredChangeEvent = true;
listenerList = listenerList.concat(changeListeners);
if (horizonEventToNativeMap.get('onSelect').includes(nativeEvtName)) {
listenerList = listenerList.concat(getSelectionListeners(
nativeEvtName,
nativeEvent,
vNode,
target,
));
} }
} }
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); processListeners(listenerList);
return hasTriggeredChangeEvent;
} }
@ -136,11 +170,11 @@ export function handleEventMain(
isCapture: boolean, isCapture: boolean,
nativeEvent: AnyNativeEvent, nativeEvent: AnyNativeEvent,
vNode: null | VNode, vNode: null | VNode,
targetContainer: EventTarget, targetDom: EventTarget,
): void { ): void {
let startVNode = vNode; let startVNode = vNode;
if (startVNode !== null) { if (startVNode !== null) {
startVNode = getExactNode(startVNode, targetContainer); startVNode = findRoot(startVNode, targetDom);
if (!startVNode) { if (!startVNode) {
return; return;
} }
@ -154,13 +188,15 @@ export function handleEventMain(
// 没有事件在执行,经过调度再执行事件 // 没有事件在执行,经过调度再执行事件
isInEventsExecution = true; isInEventsExecution = true;
let hasTriggeredChangeEvent = false;
try { try {
asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode)); hasTriggeredChangeEvent = asyncUpdates(() => triggerHorizonEvents(nativeEvtName, isCapture, nativeEvent, startVNode));
} finally { } finally {
isInEventsExecution = false; isInEventsExecution = false;
if (shouldUpdateValue()) { if (hasTriggeredChangeEvent) {
runDiscreteUpdates(); runDiscreteUpdates();
updateControlledValue(); // 若是Radio同步同组其他Radio的Handler Value
syncRadiosHandler(nativeEvent.target as Element);
} }
} }
} }

View File

@ -1,7 +1,7 @@
import { VNode } from '../renderer/Types'; import { VNode } from '../renderer/Types';
import { DomComponent } from '../renderer/vnode/VNodeTags'; import { DomComponent } from '../renderer/vnode/VNodeTags';
import { EVENT_TYPE_ALL, EVENT_TYPE_CAPTURE, EVENT_TYPE_BUBBLE } from './const';
import { AnyNativeEvent, ListenerUnitList } from './Types'; import { AnyNativeEvent, ListenerUnitList } from './Types';
import { EVENT_TYPE_ALL, EVENT_TYPE_BUBBLE, EVENT_TYPE_CAPTURE } from './EventHub';
// 从vnode属性中获取事件listener // 从vnode属性中获取事件listener
function getListenerFromVNode(vNode: VNode, eventName: string): Function | null { function getListenerFromVNode(vNode: VNode, eventName: string): Function | null {

View File

@ -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 [];
}

View File

@ -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事件
* inputtextareacontentEditable元素
*
*/
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;
}

View File

@ -1,9 +1,7 @@
export function isInputElement(dom?: HTMLElement): boolean { export function isInputElement(dom?: HTMLElement): boolean {
if (dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement) { return dom instanceof HTMLInputElement || dom instanceof HTMLTextAreaElement;
return true;
}
return false;
} }
export function setPropertyWritable(obj, propName) { export function setPropertyWritable(obj, propName) {

View File

@ -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);
}
}

View File

@ -0,0 +1,3 @@
// The two constants must be the same as those in horizon.
export const FunctionComponent = 'FunctionComponent';
export const ClassComponent = 'ClassComponent';

View File

@ -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();
}

View File

@ -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;
};
}

View File

@ -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;

View File

@ -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 {
}
}

View File

@ -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);
}
}

View File

@ -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];
}

View File

@ -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;
}

View File

@ -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;
},
};
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}

81
libs/horizon/src/horizonx/types.d.ts vendored Normal file
View File

@ -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

View File

@ -45,13 +45,28 @@ export function resetContext(providerVNode: VNode) {
context.value = providerVNode.context; context.value = providerVNode.context;
} }
// 在局部更新时,恢复父节点的context // 在局部更新时,从上到下恢复父节点的context
export function recoverParentContext(vNode: VNode) { 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; let parent = vNode.parent;
while (parent !== null) { while (parent !== null) {
if (parent.tag === ContextProvider) { if (parent.tag === ContextProvider) {
setContext(parent, parent.props.value); resetContext(parent);
} }
parent = parent.parent; parent = parent.parent;
} }

View File

@ -29,7 +29,7 @@ import {
isExecuting, isExecuting,
setExecuteMode setExecuteMode
} from './ExecuteMode'; } from './ExecuteMode';
import { recoverParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver'; import { recoverParentContext, resetParentContext, resetNamespaceCtx, setNamespaceCtx } from './ContextSaver';
import { import {
updateChildShouldUpdate, updateChildShouldUpdate,
updateParentsChildShouldUpdate, updateParentsChildShouldUpdate,
@ -43,6 +43,11 @@ let unrecoverableErrorDuringBuild: any = null;
// 当前运行的vNode节点 // 当前运行的vNode节点
let processing: VNode | null = null; let processing: VNode | null = null;
let currentRoot: VNode | null = null;
export function getCurrentRoot() {
return currentRoot;
}
export function setProcessing(vNode: VNode | null) { export function setProcessing(vNode: VNode | null) {
processing = vNode; processing = vNode;
} }
@ -258,6 +263,10 @@ function buildVNodeTree(treeRoot: VNode) {
handleError(treeRoot, thrownValue); handleError(treeRoot, thrownValue);
} }
} }
if (startVNode.tag !== TreeRoot) { // 不是根节点
// 恢复父节点的context
resetParentContext(startVNode);
}
setProcessingClassVNode(null); setProcessingClassVNode(null);
@ -267,7 +276,7 @@ function buildVNodeTree(treeRoot: VNode) {
// 总体任务入口 // 总体任务入口
function renderFromRoot(treeRoot) { function renderFromRoot(treeRoot) {
runAsyncEffects(); runAsyncEffects();
currentRoot = treeRoot;
// 1. 构建vNode树 // 1. 构建vNode树
buildVNodeTree(treeRoot); buildVNodeTree(treeRoot);
@ -278,6 +287,7 @@ function renderFromRoot(treeRoot) {
// 2. 提交变更 // 2. 提交变更
submitToRender(treeRoot); submitToRender(treeRoot);
currentRoot = null;
if (window.__HORIZON_DEV_HOOK__) { if (window.__HORIZON_DEV_HOOK__) {
const hook = window.__HORIZON_DEV_HOOK__; const hook = window.__HORIZON_DEV_HOOK__;

View File

@ -74,7 +74,11 @@ export class VNode {
suspenseState: SuspenseState; suspenseState: SuspenseState;
path = ''; // 保存从根到本节点的路径 path = ''; // 保存从根到本节点的路径
// 根节点数据
toUpdateNodes: Set<VNode> | null; // 保存要更新的节点 toUpdateNodes: Set<VNode> | null; // 保存要更新的节点
delegatedEvents: Set<string>
delegatedNativeEvents: Set<string>
belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用 belongClassVNode: VNode | null = null; // 记录JSXElement所属class vNode处理ref的时候使用
@ -94,6 +98,8 @@ export class VNode {
this.realNode = realNode; this.realNode = realNode;
this.task = null; this.task = null;
this.toUpdateNodes = new Set<VNode>(); this.toUpdateNodes = new Set<VNode>();
this.delegatedEvents = new Set<string>();
this.delegatedNativeEvents = new Set<string>();
this.updates = null; this.updates = null;
this.stateCallbacks = null; this.stateCallbacks = null;
this.state = null; this.state = null;

View File

@ -31,7 +31,6 @@ export function travelVNodeTree(
finishVNode: VNode, // 结束遍历节点有时候和beginVNode不相同 finishVNode: VNode, // 结束遍历节点有时候和beginVNode不相同
handleWhenToParent: Function | null handleWhenToParent: Function | null
): VNode | null { ): VNode | null {
const filter = childFilter === null;
let node = beginVNode; let node = beginVNode;
while (true) { while (true) {
@ -43,7 +42,7 @@ export function travelVNodeTree(
// 找子节点 // 找子节点
const childVNode = node.child; const childVNode = node.child;
if (childVNode !== null && (filter || !childFilter(node))) { if (childVNode !== null && (childFilter === null || !childFilter(node))) {
childVNode.parent = node; childVNode.parent = node;
node = childVNode; node = childVNode;
continue; 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) { function isPortalRoot(vNode, targetContainer) {
if (vNode.tag === DomPortal) { if (vNode.tag === DomPortal) {
let topVNode = vNode.parent; let topVNode = vNode.parent;
@ -216,7 +201,7 @@ function isPortalRoot(vNode, targetContainer) {
if (grandTag === TreeRoot || grandTag === DomPortal) { if (grandTag === TreeRoot || grandTag === DomPortal) {
const topContainer = topVNode.realNode; const topContainer = topVNode.realNode;
// 如果topContainer是targetContainer不需要在这里处理 // 如果topContainer是targetContainer不需要在这里处理
if (isSameContainer(topContainer, targetContainer)) { if (topContainer === targetContainer) {
return true; return true;
} }
} }
@ -228,28 +213,28 @@ function isPortalRoot(vNode, targetContainer) {
} }
// 获取根vNode节点 // 获取根vNode节点
export function getExactNode(targetVNode, targetContainer) { export function findRoot(targetVNode, targetDom) {
// 确认vNode节点是否准确portal场景下可能祖先节点不准确 // 确认vNode节点是否准确portal场景下可能祖先节点不准确
let vNode = targetVNode; let vNode = targetVNode;
while (vNode !== null) { while (vNode !== null) {
if (vNode.tag === TreeRoot || vNode.tag === DomPortal) { if (vNode.tag === TreeRoot || vNode.tag === DomPortal) {
let container = vNode.realNode; let dom = vNode.realNode;
if (isSameContainer(container, targetContainer)) { if (dom === targetDom) {
break; break;
} }
if (isPortalRoot(vNode, targetContainer)) { if (isPortalRoot(vNode, targetDom)) {
return null; return null;
} }
while (container !== null) { while (dom !== null) {
const parentNode = getNearestVNode(container); const parentNode = getNearestVNode(dom);
if (parentNode === null) { if (parentNode === null) {
return null; return null;
} }
if (parentNode.tag === DomComponent || parentNode.tag === DomText) { if (parentNode.tag === DomComponent || parentNode.tag === DomText) {
return getExactNode(parentNode, targetContainer); return findRoot(parentNode, targetDom);
} }
container = container.parentNode; dom = dom.parentNode;
} }
} }
vNode = vNode.parent; vNode = vNode.parent;

View File

@ -6,6 +6,7 @@
"scripts": { "scripts": {
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"build": " rollup --config ./scripts/rollup/rollup.config.js", "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": "node ./scripts/gen3rdLib.js",
"build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev", "build-3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev",
"build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon", "build-horizon3rdLib-dev": "npm run build & node ./scripts/gen3rdLib.js --dev --type horizon",

View File

@ -358,4 +358,79 @@ describe('Context Test', () => {
Horizon.render(<App num={8} type={'typeR'} />, container); Horizon.render(<App num={8} type={'typeR'} />, container);
expect(container.querySelector('p').innerHTML).toBe('Num: 8, Type: typeR'); 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');
});
}); });

View File

@ -30,6 +30,7 @@ describe('useEffect Hook Test', () => {
expect(document.getElementById('p').style.display).toBe('block'); expect(document.getElementById('p').style.display).toBe('block');
// 点击按钮触发num加1 // 点击按钮触发num加1
container.querySelector('button').click(); container.querySelector('button').click();
expect(document.getElementById('p').style.display).toBe('none'); expect(document.getElementById('p').style.display).toBe('none');
container.querySelector('button').click(); container.querySelector('button').click();
expect(container.querySelector('p').style.display).toBe('inline'); expect(container.querySelector('p').style.display).toBe('inline');

View File

@ -61,4 +61,10 @@ describe('Dom Attribute', () => {
container.querySelector('div').setAttribute('data-first-name', 'Tom'); container.querySelector('div').setAttribute('data-first-name', 'Tom');
expect(container.querySelector('div').dataset.firstName).toBe('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');
});
}); });

View File

@ -22,16 +22,6 @@ describe('Dom Input', () => {
).not.toThrow(); ).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属性值可以改变', () => { it('复选框的value属性值可以改变', () => {
Horizon.render( Horizon.render(
<input type='checkbox' value='' onChange={() => { <input type='checkbox' value='' onChange={() => {
@ -96,30 +86,6 @@ describe('Dom Input', () => {
).not.toThrow(); ).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值会转为字符串', () => { it('value值会转为字符串', () => {
const realNode = Horizon.render(<input type='text' value={1} />, container); const realNode = Horizon.render(<input type='text' value={1} />, container);
expect(realNode.value).toBe('1'); expect(realNode.value).toBe('1');
@ -172,6 +138,11 @@ describe('Dom Input', () => {
expect(realNode.getAttribute('value')).toBe('default'); expect(realNode.getAttribute('value')).toBe('default');
}); });
it('value为0、defaultValue为1input 的value应该为0', () => {
const input = Horizon.render(<input defaultValue={1} value={0} />, container);
expect(input.getAttribute('value')).toBe('0');
});
it('name属性', () => { it('name属性', () => {
let realNode = Horizon.render(<input type='text' name={'name'} />, container); let realNode = Horizon.render(<input type='text' name={'name'} />, container);
expect(realNode.name).toBe('name'); expect(realNode.name).toBe('name');
@ -244,30 +215,6 @@ describe('Dom Input', () => {
expect(document.getElementById('d').checked).toBe(true); 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', () => { it('name改变不影响相同name的radio', () => {
const inputRef = Horizon.createRef(); const inputRef = Horizon.createRef();
const App = () => { const App = () => {

View File

@ -53,37 +53,6 @@ describe('Dom Select', () => {
expect(realNode.value).toBe('React'); 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', () => { it('受控select转为不受控会保存原来select', () => {
const selectNode = ( const selectNode = (
<select value='Vue'> <select value='Vue'>

View File

@ -38,26 +38,6 @@ describe('Dom Textarea', () => {
expect(realNode.value).toBe('React'); 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', () => { it('设置defaultValue', () => {
let defaultVal = 'Vue'; let defaultVal = 'Vue';
const textareaNode = <textarea defaultValue={defaultVal} />; const textareaNode = <textarea defaultValue={defaultVal} />;

View File

@ -1,6 +1,13 @@
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import * as TestUtils from '../jest/testUtils'; 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('事件', () => { describe('事件', () => {
const LogUtils = TestUtils.getLogUtils(); const LogUtils = TestUtils.getLogUtils();
it('根节点挂载全量事件', () => { it('根节点挂载全量事件', () => {
@ -34,7 +41,7 @@ describe('事件', () => {
'btn capture', 'btn capture',
'btn bubble', 'btn bubble',
'p bubble', 'p bubble',
'div bubble' 'div bubble',
]); ]);
}); });
@ -46,14 +53,14 @@ describe('事件', () => {
keyCode = e.keyCode; keyCode = e.keyCode;
}} }}
/>, />,
container, container
); );
node.dispatchEvent( node.dispatchEvent(
new KeyboardEvent('keypress', { new KeyboardEvent('keypress', {
keyCode: 65, keyCode: 65,
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
}), })
); );
expect(keyCode).toBe(65); expect(keyCode).toBe(65);
}); });
@ -64,7 +71,10 @@ describe('事件', () => {
<> <>
<div onClickCapture={() => LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}> <div onClickCapture={() => LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p 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> </p>
</div> </div>
</> </>
@ -78,7 +88,7 @@ describe('事件', () => {
'div capture', 'div capture',
'p capture', 'p capture',
'btn capture', 'btn capture',
'btn bubble' 'btn bubble',
]); ]);
}); });
@ -86,7 +96,10 @@ describe('事件', () => {
const App = () => { const App = () => {
return ( 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')}> <p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={() => LogUtils.log('btn bubble')} /> <button onClickCapture={() => LogUtils.log('btn capture')} onClick={() => LogUtils.log('btn bubble')} />
</p> </p>
@ -99,7 +112,7 @@ describe('事件', () => {
expect(LogUtils.getAndClear()).toEqual([ expect(LogUtils.getAndClear()).toEqual([
// 阻止捕获,不再继续向下执行 // 阻止捕获,不再继续向下执行
'div capture' 'div capture',
]); ]);
}); });
@ -114,19 +127,148 @@ describe('事件', () => {
); );
}; };
Horizon.render(<App />, container); Horizon.render(<App />, container);
container.querySelector('div').addEventListener('click', () => { container.querySelector('div').addEventListener(
'click',
() => {
LogUtils.log('div bubble'); LogUtils.log('div bubble');
}, false); },
container.querySelector('p').addEventListener('click', () => { false
);
container.querySelector('p').addEventListener(
'click',
() => {
LogUtils.log('p bubble'); LogUtils.log('p bubble');
}, false); },
container.querySelector('button').addEventListener('click', (e) => { false
);
container.querySelector('button').addEventListener(
'click',
e => {
LogUtils.log('btn bubble'); LogUtils.log('btn bubble');
e.stopPropagation(); e.stopPropagation();
}, false); },
false
);
container.querySelector('button').click(); container.querySelector('button').click();
expect(LogUtils.getAndClear()).toEqual([ expect(LogUtils.getAndClear()).toEqual(['btn bubble']);
'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]);
// 先选择选项1radio1应该重新触发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']);
}); });
}); });

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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],
},
});

View File

@ -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");
});
});

View File

@ -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;
};
};
}

View File

@ -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);
});
});

View File

@ -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');
});
});

View File

@ -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.'
);
});
});

View File

@ -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');
});
});

View File

@ -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: ');
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -1,9 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import * as Horizon from '@cloudsop/horizon/index.ts'; import * as Horizon from '@cloudsop/horizon/index.ts';
import { getLogUtils } from './testUtils'; import { getLogUtils } from './testUtils';
export const App = (props) => { export const App = props => {
const Parent = props.parent; const Parent = props.parent;
const Child = props.child; const Child = props.child;
@ -16,8 +15,15 @@ export const App = (props) => {
); );
}; };
export const Text = (props) => { export const Text = props => {
const LogUtils =getLogUtils(); const LogUtils = getLogUtils();
LogUtils.log(props.text); LogUtils.log(props.text);
return <p id={props.id}>{props.text}</p>; 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);
}

View File

@ -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'; //import * as LogUtils from './logUtils';
export const stopBubbleOrCapture = (e, value) => { export const stopBubbleOrCapture = (e, value) => {

View File

@ -1,6 +1,7 @@
import nodeResolve from '@rollup/plugin-node-resolve'; import nodeResolve from '@rollup/plugin-node-resolve';
import babel from '@rollup/plugin-babel'; import babel from '@rollup/plugin-babel';
import path from 'path'; import path from 'path';
import fs from 'fs';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import copy from './copy-plugin'; import copy from './copy-plugin';
import { terser } from 'rollup-plugin-terser'; import { terser } from 'rollup-plugin-terser';
@ -11,6 +12,14 @@ const extensions = ['.js', '.ts'];
const libDir = path.join(__dirname, '../../libs/horizon'); const libDir = path.join(__dirname, '../../libs/horizon');
const rootDir = path.join(__dirname, '../..'); const rootDir = path.join(__dirname, '../..');
const outDir = path.join(rootDir, 'build', 'horizon'); 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); const outputResolve = (...p) => path.resolve(outDir, ...p);
function genConfig(mode) { function genConfig(mode) {
@ -52,7 +61,7 @@ function genConfig(mode) {
mode === 'production' && terser(), mode === 'production' && terser(),
copy([ copy([
{ {
from: path.join(libDir, 'index.js'), from: path.join(libDir, '/npm/index.js'),
to: path.join(outDir, 'index.js'), to: path.join(outDir, 'index.js'),
}, },
{ {

File diff suppressed because one or more lines are too long