Match-id-0a9b956d94031f8d8f03fb90913325154133063d

This commit is contained in:
* 2022-09-19 17:19:07 +08:00
parent b20293076c
commit fbf7e8370b
10 changed files with 506 additions and 70 deletions

View File

@ -32,34 +32,34 @@ export type ReduxMiddleware = (
type Reducer = (state: any, action: ReduxAction) => any;
function mergeData(state,data){
console.log('merging data',{state,data});
if(!data){
function mergeData(state, data) {
console.log('merging data', { state, data });
if (!data) {
console.log('!data');
state.stateWrapper=data;
state.stateWrapper = data;
return;
}
if(Array.isArray(data) && Array.isArray(state?.stateWrapper)){
if (Array.isArray(data) && Array.isArray(state?.stateWrapper)) {
console.log('data is array');
state.stateWrapper.length = data.length;
data.forEach((item,idx) => {
if(item!=state.stateWrapper[idx]){
state.stateWrapper[idx]=item;
data.forEach((item, idx) => {
if (item != state.stateWrapper[idx]) {
state.stateWrapper[idx] = item;
}
});
return;
}
if(typeof data === 'object' && typeof state?.stateWrapper === 'object'){
if (typeof data === 'object' && typeof state?.stateWrapper === 'object') {
console.log('data is object');
Object.keys(state.stateWrapper).forEach(key => {
if(!data.hasOwnProperty(key)) delete state.stateWrapper[key];
})
if (!data.hasOwnProperty(key)) delete state.stateWrapper[key];
});
Object.entries(data).forEach(([key,value])=>{
if(state.stateWrapper[key]!==value){
state.stateWrapper[key]=value;
Object.entries(data).forEach(([key, value]) => {
if (state.stateWrapper[key] !== value) {
state.stateWrapper[key] = value;
}
});
return;
@ -100,7 +100,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?):
const result = {
reducer,
getState: function() {
getState: function () {
return store.$s.stateWrapper;
},
subscribe: listener => {
@ -169,7 +169,7 @@ export function bindActionCreators(actionCreators: ActionCreators, dispatch: Dis
return boundActionCreators;
}
export function compose(middlewares: ReduxMiddleware[]) {
export function compose(...middlewares: ReduxMiddleware[]) {
return (store: ReduxStoreHandler, extraArgument: any) => {
let val;
middlewares.reverse().forEach((middleware: ReduxMiddleware, index) => {

View File

@ -4,7 +4,8 @@ import { createContext } from '../../renderer/components/context/CreateContext';
import { createElement } from '../../external/JSXElement';
import { BoundActionCreator } from './redux';
import { ReduxAction } from './redux';
import { ReduxStoreHandler } from '../store/StoreHandler'
import { ReduxStoreHandler } from '../store/StoreHandler';
import { VNode } from '../../renderer/Types';
const DefaultContext = createContext(null);
type Context = typeof DefaultContext;
@ -22,15 +23,15 @@ export function Provider({
return createElement(Context.Provider, { value: store }, children);
}
export function createStoreHook(context: Context) {
export function createStoreHook(context: Context): () => ReduxStoreHandler {
return () => {
return useContext(context);
return useContext(context) as unknown as ReduxStoreHandler;
};
}
export function createSelectorHook(context: Context): (selector?: (any) => any) => any {
const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler;
return function(selector = state => state) {
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
return function (selector = state => state) {
const [b, fr] = useState(false);
useEffect(() => {
@ -44,9 +45,9 @@ export function createSelectorHook(context: Context): (selector?: (any) => any)
};
}
export function createDispatchHook(context: Context): ()=>BoundActionCreator {
const store = (createStoreHook(context)() as unknown) as ReduxStoreHandler;
return function() {
export function createDispatchHook(context: Context): () => BoundActionCreator {
const store = createStoreHook(context)() as unknown as ReduxStoreHandler;
return function () {
return action => {
store.dispatch(action);
};
@ -84,28 +85,46 @@ export const useStore = () => {
// 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,
type MapStateToPropsP<StateProps, OwnProps> = (state: any, ownProps: OwnProps) => StateProps;
type MapDispatchToPropsP<DispatchProps, OwnProps> =
| { [key: string]: (...args: any[]) => ReduxAction }
| ((dispatch: (action: ReduxAction) => any, ownProps: OwnProps) => DispatchProps);
type MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
stateProps: StateProps,
dispatchProps: DispatchProps,
ownProps: OwnProps
) => MergedProps;
type WrappedComponent<OwnProps> = (props: OwnProps) => ReturnType<typeof createElement>;
type OriginalComponent<MergedProps> = (props: MergedProps) => ReturnType<typeof createElement>;
type Connector<OwnProps, MergedProps> = (Component: OriginalComponent<MergedProps>) => WrappedComponent<OwnProps>;
export function connect<StateProps, DispatchProps, OwnProps, MergedProps>(
mapStateToProps: MapStateToPropsP<StateProps, OwnProps> = () => ({} as StateProps),
mapDispatchToProps: MapDispatchToPropsP<DispatchProps, OwnProps> = () => ({} as DispatchProps),
mergeProps: MergePropsP<StateProps, DispatchProps, OwnProps, MergedProps> = (
stateProps,
dispatchProps,
ownProps
): MergedProps => ({ ...stateProps, ...dispatchProps, ...ownProps } as MergedProps),
options?: {
areStatesEqual?: (oldState: any, newState: any) => boolean;
context?: any; // TODO: type this
context?: Context;
}
) {
): Connector<OwnProps, MergedProps> {
if (!options) {
options = {};
}
return Component => {
//this component should bear the type returned from mapping functions
return (Component: OriginalComponent<MergedProps>): WrappedComponent<OwnProps> => {
const useStore = createStoreHook(options?.context || DefaultContext);
function Wrapper(props) {
//this component should mimic original type of component used
const Wrapper: WrappedComponent<OwnProps> = (props: OwnProps) => {
const [f, forceReload] = useState(true);
const store = (useStore() as unknown) as ReduxStoreHandler;
const store = useStore();
useEffect(() => {
const unsubscribe = store.subscribe(() => forceReload(!f));
@ -119,36 +138,34 @@ export function connect(
mappedState: {},
}) as {
current: {
state: {};
mappedState: {};
state: { [key: string]: any };
mappedState: StateProps;
};
};
let mappedState;
let mappedState: StateProps;
if (options?.areStatesEqual) {
if (options.areStatesEqual(previous.current.state, store.getState())) {
mappedState = previous.current.mappedState;
mappedState = previous.current.mappedState as StateProps;
} else {
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
previous.current.mappedState = mappedState;
}
} else {
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : {};
mappedState = mapStateToProps ? mapStateToProps(store.getState(), props) : ({} as StateProps);
previous.current.mappedState = mappedState;
}
let mappedDispatch: { dispatch?: (action) => void } = {};
let mappedDispatch: DispatchProps = {} as DispatchProps;
if (mapDispatchToProps) {
if (typeof mapDispatchToProps === 'object') {
Object.entries(mapDispatchToProps).forEach(([key, value]) => {
mappedDispatch[key] = (...args) => {
mappedDispatch[key] = (...args: ReduxAction[]) => {
store.dispatch(value(...args));
};
});
} else {
mappedDispatch = mapDispatchToProps(store.dispatch, props);
}
} else {
mappedDispatch.dispatch = store.dispatch;
}
const mergedProps = (
mergeProps ||
@ -161,7 +178,7 @@ export function connect(
const node = createElement(Component, mergedProps);
return node;
}
};
return Wrapper;
};

View File

@ -0,0 +1,8 @@
export function watch(stateVariable:any,listener:(stateVariable:any)=>void){
listener = listener.bind(null,stateVariable);
stateVariable.addListener(listener);
return ()=>{
stateVariable.removeListener(listener);
}
}

View File

@ -35,9 +35,7 @@ type StoreHandler<S extends object, A extends UserActions<S>, C extends UserComp
$queue: QueuedStoreActions<S, A>;
$a: StoreActions<S, A>;
$c: UserComputedValues<S>;
} & { [K in keyof S]: S[K] } &
{ [K in keyof A]: Action<A[K], S> } &
{ [K in keyof C]: ReturnType<C[K]> };
} & { [K in keyof S]: S[K] } & { [K in keyof A]: Action<A[K], S> } & { [K in keyof C]: ReturnType<C[K]> };
type PlannedAction<S extends object, F extends ActionFunction<S>> = {
action: string;
@ -76,6 +74,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
//create a local shalow copy to ensure consistency (if user would change the config object after store creation)
config = {
id: config.id,
/* @ts-ignore*/
options: config.options,
state: config.state,
actions: config.actions ? { ...config.actions } : undefined,
@ -87,6 +86,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
throw new Error('store obj must be pure object');
}
/* @ts-ignore*/
const proxyObj = createProxy(config.state, !config.options?.reduxAdapter);
proxyObj.$pending = false;
@ -103,7 +103,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
const $a: Partial<StoreActions<S, A>> = {};
const $queue: Partial<StoreActions<S, A>> = {};
const $c: Partial<ComputedValues<S, C>> = {};
const handler = ({
const handler = {
$subscribe,
$unsubscribe,
$a: $a as StoreActions<S, A>,
@ -111,7 +111,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
$c: $c as ComputedValues<S, C>,
$config: config,
$queue: $queue as QueuedStoreActions<S, A>,
} as unknown) as StoreHandler<S, A, C>;
} as unknown as StoreHandler<S, A, C>;
function tryNextAction() {
if (!plannedActions.length) {
@ -193,6 +193,9 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
Object.keys(config.state).forEach(key => {
Object.defineProperty(handler, key, {
get: () => proxyObj[key],
set: value => {
proxyObj[key] = value;
},
});
});
}
@ -205,7 +208,7 @@ export function createStore<S extends object, A extends UserActions<S>, C extend
}
export function clearVNodeObservers(vNode) {
if(!vNode.observers) return;
if (!vNode.observers) return;
vNode.observers.forEach(observer => {
observer.clearByVNode(vNode);
});
@ -220,14 +223,14 @@ function hookStore() {
if (!processingVNode) {
return;
}
if (!processingVNode.observers) {
processingVNode.observers = new Set<Observer>();
}
if (processingVNode.tag === FunctionComponent) {
// from FunctionComponent
const vNodeRef = (useRef(null) as unknown) as { current: VNode };
const vNodeRef = useRef(null) as unknown as { current: VNode };
vNodeRef.current = processingVNode;
useEffect(() => {
@ -239,7 +242,7 @@ function hookStore() {
} else if (processingVNode.tag === ClassComponent) {
// from ClassComponent
if (!processingVNode.classComponentWillUnmount) {
processingVNode.classComponentWillUnmount = function(vNode) {
processingVNode.classComponentWillUnmount = function (vNode) {
clearVNodeObservers(vNode);
vNode.observers = null;
};

View File

@ -34,11 +34,11 @@
"@babel/plugin-transform-object-super": "7.16.7",
"@babel/plugin-transform-parameters": "7.16.7",
"@babel/plugin-transform-react-jsx": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/plugin-transform-runtime": "7.16.7",
"@babel/plugin-transform-shorthand-properties": "7.16.7",
"@babel/plugin-transform-spread": "7.16.7",
"@babel/plugin-transform-template-literals": "7.16.7",
"@babel/plugin-transform-react-jsx-source": "^7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"@rollup/plugin-babel": "^5.3.1",
@ -66,5 +66,8 @@
"engines": {
"node": ">=10.x",
"npm": ">=7.x"
},
"dependencies": {
"ejs": "^3.1.8"
}
}

View File

@ -36,6 +36,34 @@ describe('Basic store manipulation', () => {
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1');
});
it('Should use direct setters', () => {
function App() {
const logStore = useLogStore();
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
logStore.logs = ['q'];
}}
>
add
</button>
<p id={RESULT_ID}>{logStore.logs[0]}</p>
</div>
);
}
Horizon.render(<App />, container);
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('q');
});
it('Should use actions and update components', () => {
function App() {
const logStore = useLogStore();
@ -74,7 +102,7 @@ describe('Basic store manipulation', () => {
increment: state => {
state.count++;
},
doublePlusOne: function(state) {
doublePlusOne: function (state) {
state.count = state.count * 2;
this.increment();
},
@ -115,20 +143,20 @@ describe('Basic store manipulation', () => {
count: 2,
},
actions: {
doublePlusOne: function(state) {
doublePlusOne: function (state) {
state.count = this.double + 1;
},
},
computed:{
double: (state) => {
return state.count*2
}
}
computed: {
double: state => {
return state.count * 2;
},
},
});
function App() {
const incrementStore = useIncrementStore();
return (
<div>
<button
@ -143,13 +171,13 @@ describe('Basic store manipulation', () => {
</div>
);
}
Horizon.render(<App />, container);
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('5');
})
});
});

View File

@ -0,0 +1,132 @@
import { createStore } from "@cloudsop/horizon/src/horizonx/store/StoreHandler";
import { watch } from "@cloudsop/horizon/src/horizonx/proxy/watch";
describe("watch",()=>{
it('shouhld watch promitive state variable', async()=>{
const useStore = createStore({
state:{
variable:'x'
},
actions:{
change:(state)=>state.variable = "a"
}
});
const store = useStore();
let counter = 0;
watch(store.$s,(state)=>{
counter++;
expect(state.variable).toBe('a');
})
store.change();
expect(counter).toBe(1);
});
it('shouhld watch object variable', async()=>{
const useStore = createStore({
state:{
variable:'x'
},
actions:{
change:(state)=>state.variable = "a"
}
});
const store = useStore();
let counter = 0;
store.$s.watch('variable',()=>{
counter++;
})
store.change();
expect(counter).toBe(1);
});
it('shouhld watch array item', async()=>{
const useStore = createStore({
state:{
arr:['x']
},
actions:{
change:(state)=>state.arr[0]='a'
}
});
const store = useStore();
let counter = 0;
store.arr.watch('0',()=>{
counter++;
})
store.change();
expect(counter).toBe(1);
});
it('shouhld watch collection item', async()=>{
const useStore = createStore({
state:{
collection:new Map([
['a', 'a'],
])
},
actions:{
change:(state)=>state.collection.set('a','x')
}
});
const store = useStore();
let counter = 0;
store.collection.watch('a',()=>{
counter++;
})
store.change();
expect(counter).toBe(1);
});
it('should watch multiple variables independedntly', async()=>{
const useStore = createStore({
state:{
bool1:true,
bool2:false
},
actions:{
toggle1:state=>state.bool1=!state.bool1,
toggle2:state=>state.bool2=!state.bool2
}
});
let counter1=0;
let counterAll=0;
const store = useStore();
watch(store.$s,()=>{
counterAll++;
})
store.$s.watch('bool1',()=>{
counter1++;
});
store.toggle1();
store.toggle1();
store.toggle2();
store.toggle1();
store.toggle2();
store.toggle2();
expect(counter1).toBe(3);
expect(counterAll).toBe(6);
})
})

View File

@ -293,4 +293,4 @@ describe('Redux/React binding adapter', () => {
expect(getE(BUTTON).innerHTML).toBe('1');
expect(getE(BUTTON2).innerHTML).toBe('true');
});
});
});

View File

@ -0,0 +1,46 @@
import { createElement } from '../../../../libs/horizon/src/external/JSXElement';
import { createDomTextVNode } from '../../../../libs/horizon/src/renderer/vnode/VNodeCreator';
import { createStore } from '../../../../libs/horizon/src/horizonx/adapters/redux';
import { connect } from '../../../../libs/horizon/src/horizonx/adapters/reduxReact';
createStore((state: number = 0, action): number => {
if (action.type === 'add') return state + 1;
return 0;
});
type WrappedButtonProps = { add: () => void; count: number; text: string };
function Button(props: WrappedButtonProps) {
const { add, count, text } = props;
return createElement(
'button',
{
onClick: add,
},
createDomTextVNode(text),
createDomTextVNode(': '),
createDomTextVNode(count)
);
}
const connector = connect(
state => ({ count: state }),
dispatch => ({
add: (): void => {
dispatch({ type: 'add' });
},
}),
(stateProps, dispatchProps, ownProps: { text: string }) => ({
add: dispatchProps.add,
count: stateProps.count,
text: ownProps.text,
})
);
const ConnectedButton = connector(Button);
function App() {
return createElement('div', {}, createElement(ConnectedButton, { text: 'click' }));
}
export default App;

View File

@ -0,0 +1,199 @@
//@ts-ignore
import Horizon, { createStore } from '@cloudsop/horizon/index.ts';
import { triggerClickEvent } from '../../jest/commonComponents';
import { describe, beforeEach, afterEach, it, expect } from '@jest/globals';
const { unmountComponentAtNode } = Horizon;
const useStore1 = createStore({
state:{ counter:1 },
actions:{
add:(state)=>state.counter++,
reset: (state)=>state.counter=1
}
})
const useStore2 = createStore({
state:{ counter2:1 },
actions:{
add2:(state)=>state.counter2++,
reset: (state)=>state.counter2=1
}
})
describe('Using multiple stores', () => {
let container: HTMLElement | null = null;
const BUTTON_ID = 'btn';
const BUTTON_ID2 = 'btn2';
const RESULT_ID = 'result';
beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
useStore1().reset();
useStore2().reset();
});
afterEach(() => {
unmountComponentAtNode(container);
container?.remove();
container = null;
});
it('Should use multiple stores in class component', () => {
class App extends Horizon.Component{
render(){
const {counter,add} = useStore1();
const {counter2, add2} = useStore2();
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
add2();
}}
>
add
</button>
<p id={RESULT_ID}>{counter} {counter2}</p>
</div>
)
}
}
Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
});
it('Should use use stores in cycles and multiple methods', () => {
interface App {
store:any,
store2:any
}
class App extends Horizon.Component{
constructor(){
super();
this.store = useStore1();
this.store2 = useStore2()
}
render(){
const {counter,add} = useStore1();
const store2 = useStore2();
const {counter2, add2} = store2;
for(let i=0; i<100; i++){
const {counter,add} = useStore1();
const store2 = useStore2();
const {counter2, add2} = store2;
}
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
this.store2.add2();
}}
>
add
</button>
<p id={RESULT_ID}>{counter} {counter2}</p>
</div>
)
}
}
Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
});
it('Should use multiple stores in function component', () => {
function App() {
const {counter,add} = useStore1();
const store2 = useStore2();
const {counter2, add2} = store2;
return (
<div>
<button
id={BUTTON_ID}
onClick={() => {
add();
}}
>
add
</button>
<button
id={BUTTON_ID2}
onClick={() => {
add2();
}}
>
add
</button>
<p id={RESULT_ID}>{counter} {counter2}</p>
</div>
);
}
Horizon.render(<App />, container);
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('1 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 1');
Horizon.act(() => {
triggerClickEvent(container, BUTTON_ID2);
});
expect(document.getElementById(RESULT_ID)?.innerHTML).toBe('2 2');
});
});