!143 test(no-vdom): Cond

* test(no-vdom): Cond
This commit is contained in:
Hoikan 2024-02-18 06:42:02 +00:00 committed by 陈超涛
parent 80f3be9436
commit 521344f8ff
11 changed files with 679 additions and 12 deletions

View File

@ -24,15 +24,13 @@ type Branch = JSXElement | (() => JSXElement);
export interface CondProps {
// Array of tuples, first item is the condition, second is the branch to render
branches: [CondExpression, Branch];
branches: [CondExpression, Branch][];
}
export function Cond(props: CondProps) {
// Find the first branch that matches the condition
// Any signal that used in condition expression, will trigger the condition to recompute
const currentBranch = computed(() => {
// clean up the previous branch
for (let i = 0; i < props.branches.length; i++) {
const [condition, branch] = props.branches[i];
if (typeof condition === 'function' ? condition() : condition) {

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { mergeProps, splitProps } from '../util';
import { Context, WithChildren } from '../type';
// Reactive scope for cleanup and context
export interface Scope<T = Context> {
context: T;
}
// The current scope for the current render
let scope: Scope | null = null;
export function runWithScope<T>(fn: () => T, context: Context): T {
const prevScope = scope;
// Merge the context with the previous context for nested env
let mergedContext = context;
if (prevScope?.context) {
mergedContext = mergeProps(prevScope.context, context);
}
// Set the new scope
scope = {
context: mergedContext,
};
try {
// Run the children with the new scope
return fn();
} finally {
// Restore the previous scope
scope = prevScope;
}
}
export function readContext() {
return scope?.context || {};
}
type EnvProps<ContextType> = WithChildren<ContextType>;
/**
* @description The Env component is used to provide a context for the children components.
* @example
* type ThemeContext = { theme: string };
* <env<ThemeContext> theme="dark">
* <Comp />
* </env>
* When Comp is rendered, it can access the theme from the env.
* function Comp(props, context: UserContext & ThemeContext) {
* return <h1>{theme}</h1>
* }
*/
export function Env<ContextType = Context>(props: EnvProps<ContextType>) {
// splitProps is used to extract the children from the props.
// because when we directly access the children, we will trigger the children getter function.
const [childrenProp, context] = splitProps(props, ['children']);
return runWithScope(() => childrenProp.children, context);
}

View File

@ -15,12 +15,16 @@
import { insert } from './dom';
import { untrack } from 'inula-reactive';
import { readContext } from './components/Env';
type ComponentConstructor<T> = (props: T) => any;
type ComponentConstructor<T> = (props: T, context: any) => any;
type CodeFunction = () => any;
export function runComponent<T>(Comp: ComponentConstructor<T>, props: T = {} as T): any {
return untrack(() => Comp(props));
return untrack(() => {
const context = readContext();
return Comp(props, context);
});
}
const ELEMENT_NODE = 1;

View File

@ -15,5 +15,8 @@
// TODO: JSX type
export type JSXElement = any;
export type FunctionComponent<Props = Record<string, unknown>> = (props: Props) => unknown;
export type FunctionComponent<P = Props> = (props: P) => unknown;
export type AppDisposer = () => void;
export type Props = Record<string, unknown>;
export type Context = Props;
export type WithChildren<T = Props> = T & { children?: JSXElement };

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { Props } from './type';
/**
* split props into different objects, should copy getter and setter descriptor.
* @example
* splitProps({a: 1, get b() { return 2 }}, ['a']) // => [{a: 1}, {get b() { return 2 }}]
*/
export function splitProps(props: Props, keys: string[]): [Props, Props] {
const target: Props = {};
const others: Props = {};
for (const propName of Object.getOwnPropertyNames(props)) {
copyProp(props, keys.includes(propName) ? target : others, propName);
}
return [target, others];
}
function copyProp(resource: Props, target: Props, key: string): void {
const desc = Object.getOwnPropertyDescriptor(resource, key)!;
if (!desc.get && !desc.set && desc.enumerable && desc.writable && desc.configurable) {
// if the prop is a plain value, we can directly assign it to the context
target[key] = desc.value;
} else {
Object.defineProperty(target, key, desc);
}
}
/**
* merge two props into one object, including getter and setter descriptor.
* @param a
* @param b
* @example
* mergeProps({get a() { return 1 }}, {get b() { return 2 }}) // => {get a() { return 1 }, get b() { return 2 }}
*/
export function mergeProps(a: Props, b: Props): Props {
const target: Props = {};
for (const propName of Object.getOwnPropertyNames(a)) {
copyProp(a, target, propName);
}
for (const propName of Object.getOwnPropertyNames(b)) {
copyProp(b, target, propName);
}
return target;
}

View File

@ -0,0 +1,412 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck For the compiled code.
import { describe, expect } from 'vitest';
import { domTest as it } from './utils';
import { template as $$template, insert as $$insert } from '../src/dom';
import { Cond } from '../src/components/Cond';
import { runComponent as $$runComponent, render, render as $$render } from '../src/core';
import { reactive } from 'inula-reactive';
import { Env } from '../src/components/Env';
describe('env', () => {
it('should work.', ({container}) => {
/**
*
* const fn = vi.fn();
* function App() {
* const x = reactive(7);
* const theme = reactive('dark');
* return (
* <env theme={theme.get()}>
* <Child />
* </env>
* );
* }
*
* function Child(props, context) {
* return <div>{context.theme}</div>;
* }
* render(() => <App />, container);
*/
// 编译后:
const theme = reactive('dark');
function App() {
return $$runComponent(Env, {
get children() {
return $$runComponent(Child);
},
get theme() {
return theme.get();
},
});
}
const _tmpl$ = /*#__PURE__*/ $$template(`<div>`);
function Child(props, context) {
return (() => {
const _el$ = _tmpl$();
$$insert(_el$, () => context.theme);
return _el$;
})();
}
render(() => $$runComponent(App, {}), container);
expect(container.innerHTML).toBe('<div>dark</div>');
theme.set('light');
expect(container.innerHTML).toBe('<div>light</div>');
});
it('should work with nested env.', ({container}) => {
/**
*
* const fn = vi.fn();
* function App() {
* const theme = reactive('dark');
* return (
* <env theme={theme.get()}>
* <Child />
* </env>
* );
* }
*
* function Child(props, context) {
* return (
* <env theme={context.theme}>
* <GrandChild />
* </env>
* );
* }
*
* function GrandChild(props, context) {
* return <div>{context.theme}</div>;
* }
* render(() => <App />, container);
*/
// 编译后:
const theme = reactive('dark');
function App() {
return $$runComponent(Env, {
get children() {
return $$runComponent(Child);
},
get theme() {
return theme.get();
},
});
}
const _tmpl$2 = /*#__PURE__*/ $$template(`<div>`);
function Child(props, context) {
return $$runComponent(Env, {
get children() {
return $$runComponent(GrandChild);
},
get theme() {
return context.theme;
},
});
}
function GrandChild(props, context) {
return (() => {
const _el$ = _tmpl$2();
$$insert(_el$, () => context.theme);
return _el$;
})();
}
render(() => $$runComponent(App, {}), container);
expect(container.innerHTML).toBe('<div>dark</div>');
theme.set('light');
expect(container.innerHTML).toBe('<div>light</div>');
});
it('should merged the parent env.', ({container}) => {
/**
*
* const fn = vi.fn();
* function App() {
* const theme = reactive('dark');
* return (
* <env theme={theme.get()}>
* <env name="inula">
* <env userInfo={{id: 1}}>
* <Child />
* </env>
* </env>
* </env>
* );
* }
* function Child(props, context) {
* return <div>Theme: {context.theme}, Name: {context.name}, Id: {context.userInfo.id}</div>;
* }
*/
// 编译后:
const theme = reactive('dark');
function App() {
return $$runComponent(Env, {
get children() {
return $$runComponent(Env, {
get children() {
return $$runComponent(Env, {
get children() {
return $$runComponent(Child);
},
get userInfo() {
return {id: 1};
},
get name() {
return 'inula';
},
});
},
get name() {
return 'inula';
},
});
},
get theme() {
return theme.get();
},
});
}
const _tmpl$ = /*#__PURE__*/$$template(`<div>Theme: <!>, Name: <!>, Id: `);
function Child(props, context) {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild,
_el$5 = _el$2.nextSibling,
_el$3 = _el$5.nextSibling,
_el$6 = _el$3.nextSibling,
_el$4 = _el$6.nextSibling;
$$insert(_el$, () => context.theme, _el$5);
$$insert(_el$, () => context.name, _el$6);
$$insert(_el$, () => context.userInfo.id, null);
return _el$;
})();
}
render(() => $$runComponent(App, {}), container);
expect(container.innerHTML).toBe('<div>Theme: dark, Name: inula, Id: 1</div>');
});
it('should not be affected by parrallel env.', ({container}) => {
/**
*
* function App() {
* const theme = reactive('dark');
* return (
* <>
* <env theme={theme.get()}>
* <Child />
* </env>
* <env theme="light">
* <Child />
* </env>
* </>
* );
* }
* function Child(props, context) {
* return <div>{context.theme}</div>;
* }
* render(() => <App />, container);
*/
// 编译后:
const theme = reactive('dark');
function App() {
return [
$$runComponent(Env, {
get children() {
return $$runComponent(Child);
},
get theme() {
return theme.get();
},
}),
$$runComponent(Env, {
get children() {
return $$runComponent(Child);
},
theme: 'light',
}),
];
}
const _tmpl$ = /*#__PURE__*/ $$template(`<div>`);
function Child(props, context) {
return (() => {
const _el$ = _tmpl$();
$$insert(_el$, () => context.theme);
return _el$;
})();
}
render(() => $$runComponent(App, {}), container);
expect(container.innerHTML).toBe('<div>dark</div><div>light</div>');
});
it('should work with recursive env, like menu.', ({container}) => {
/**
*
* const fn = vi.fn();
* function App() {
* return (
* <Menu key="root">
* <SubMenu key="sub1">
* <MenuItem key="1" />
* <MenuItem key="2" />
* </SubMenu>
* <SubMenu key="sub2">
* <MenuItem key="3" />
* <SubMenu key="sub3">
* <MenuItem key="4" />
* </SubMenu>
* <MenuItem key="5" />
* </SubMenu>
* </Menu>
* );
* }
*
* function Menu(props, context) {
* return (
* <env path={[props.key]}>
* <ul>
* <li>{props.key}</li>
* {props.children}
* </ul>
* </env>
* );
* }
* function SubMenu(props, context) {
* return (
* <env path={[...context.path, props.key]}>
* <ul>
* <li>{props.key}</li>
* {props.children}
* </ul>
* </env>
* );
* }
* function MenuItem(props, context) {
* return <li>{[...context.path, props.key].join('-')}</li>;
* }
* render(() => <App />, container);
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template(`<ul><li>`),
_tmpl$2 = /*#__PURE__*/ $$template(`<li>`);
function App() {
return $$runComponent(Menu, {
key: 'root',
get children() {
return [
$$runComponent(SubMenu, {
key: 'sub1',
get children() {
return [
$$runComponent(MenuItem, {
key: '1',
}),
$$runComponent(MenuItem, {
key: '2',
}),
];
},
}),
$$runComponent(SubMenu, {
key: 'sub2',
get children() {
return [
$$runComponent(MenuItem, {
key: '3',
}),
$$runComponent(SubMenu, {
key: 'sub3',
get children() {
return $$runComponent(MenuItem, {
key: '4',
});
},
}),
$$runComponent(MenuItem, {
key: '5',
}),
];
},
}),
];
},
});
}
function Menu(props, context) {
return $$runComponent(Env, {
get path() {
return [props.key];
},
get children() {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
$$insert(_el$2, () => props.key);
$$insert(_el$, () => props.children, null);
return _el$;
},
});
}
function SubMenu(props, context) {
return $$runComponent(Env, {
get path() {
return [...context.path, props.key];
},
get children() {
const _el$3 = _tmpl$(),
_el$4 = _el$3.firstChild;
$$insert(_el$4, () => props.key);
$$insert(_el$3, () => props.children, null);
return _el$3;
},
});
}
function MenuItem(props, context) {
return (() => {
const _el$5 = _tmpl$2();
$$insert(_el$5, () => [...context.path, props.key].join('-'));
return _el$5;
})();
}
render(() => $$runComponent(App, {}), container);
expect(container.innerHTML).toMatchInlineSnapshot(
'<ul><li>root</li><ul><li>sub1</li><li>root-sub1-1</li><li>root-sub1-2</li></ul><ul><li>sub2</li><li>root-sub2-3</li><ul><li>sub3</li><li>root-sub2-sub3-4</li></ul><li>root-sub2-5</li></ul></ul>',
);
});
});

View File

@ -49,7 +49,7 @@ describe('render', () => {
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.');
});
it('should render jsx with slots', ({ container }) => {
it('should render jsx expression with slots', ({ container }) => {
/**
*
* const CountingComponent = () => {
@ -123,7 +123,7 @@ describe('render', () => {
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
it('should render sub components', ({ container }) => {
it('should render components', ({ container }) => {
/**
*
* const CountValue = (props) => {
@ -185,7 +185,81 @@ describe('render', () => {
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
});
it('should render nested components', ({ container }) => {
it('should render components with slot', ({ container }) => {
/**
*
* const CountValue = (props) => {
* return <div>Title: {props.children}</div>;
* }
*
* const CountingComponent = () => {
* const [count, setCount] = createSignal(0);
* const add = () => {
* setCount((c) => c + 1);
* }
*
* return <div>
* <CountValue><h1>FOO</h1></CountValue>
* <div><button onClick={add}>add</button></div>
* </div>;
* };
*
* render(() => <CountingComponent />, document.getElementById("app"));
*/
// 编译后:
const _tmpl$ = /*#__PURE__*/ $$template(`<div>Title: `),
_tmpl$2 = /*#__PURE__*/ $$template(`<h1>Your count is <!>.`),
_tmpl$3 = /*#__PURE__*/ $$template(`<div><div><button>add`);
const CountValue = props => {
return (() => {
const _el$ = _tmpl$(),
_el$2 = _el$.firstChild;
$$insert(_el$, () => props.children, null);
return _el$;
})();
};
const CountingComponent = () => {
const count = reactive(0);
const add = () => {
count.set(c => c + 1);
};
return (() => {
const _el$3 = _tmpl$3(),
_el$8 = _el$3.firstChild,
_el$9 = _el$8.firstChild;
$$insert(
_el$3,
$$runComponent(CountValue, {
get children() {
const _el$4 = _tmpl$2(),
_el$5 = _el$4.firstChild,
_el$7 = _el$5.nextSibling,
_el$6 = _el$7.nextSibling;
$$insert(_el$4, count, _el$7);
return _el$4;
},
}),
_el$8
);
$$on(_el$9, 'click', add, true);
return _el$3;
})();
};
render(() => $$runComponent(CountingComponent, {}), container);
$$delegateEvents(['click']);
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><div>Title: <h1>Your count is 0<!---->.</h1></div><div><button>add</button></div></div>"`
);
container.querySelector('button').click();
expect(container.innerHTML).toMatchInlineSnapshot(
`"<div><div>Title: <h1>Your count is 1<!---->.</h1></div><div><button>add</button></div></div>"`
);
});
it('should render sub components', ({ container }) => {
/**
*
* const CountValue = (props) => {

View File

@ -306,7 +306,7 @@ export function runEffects(): void {
}
// 不进行响应式数据的使用追踪
export function untrack(fn: () => void) {
export function untrack<T>(fn: () => T): T {
if (runningRNode === null) {
return fn();
}

View File

@ -22,7 +22,8 @@ import { getRNodeVal } from './RNodeAccessor';
export function createReactive<T extends string>(raw?: T): Signal<string>;
export function createReactive<T extends number>(raw?: T): Signal<number>;
export function createReactive<T extends symbol>(raw?: T): Signal<symbol>;
export function createReactive<T extends number | string | symbol>(raw?: T): Signal<T>;
export function createReactive<T extends boolean>(raw?: T): Signal<boolean>;
export function createReactive<T extends number | string | symbol | boolean>(raw?: T): Signal<T>;
export function createReactive<T extends Record<any, any> | Array<any> | symbol>(raw?: T): DeepReactive<T>;
export function createReactive<T extends NonFunctionType>(raw: T, deep = true): DeepReactive<T> | Signal<T> {
// Function, Date, RegExp, null, undefined are simple signals

View File

@ -13,7 +13,7 @@
* See the Mulan PSL v2 for more details.
*/
import { createComputed as computed, createReactive as reactive, createWatch as watch} from './RNodeCreator';
import { createComputed as computed, createReactive as reactive, createWatch as watch } from './RNodeCreator';
import { isReactiveObj } from './Utils';
import { RNode, untrack, runEffects } from './RNode';
import { setScheduler } from './SetScheduler';

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import { reactive, computed, watch } from '../src';
describe('watch', () => {
it('should not track the unused reactive', () => {
const cond = reactive(true);
const a = reactive(1);
const b = reactive(1);
const fn = jest.fn();
watch(() => {
fn();
if (cond.get()) {
a.get();
} else {
b.get();
}
});
expect(fn).toBeCalledTimes(1);
a.set(2);
expect(fn).toBeCalledTimes(2);
b.set(2);
// should not trigger fn
expect(fn).toBeCalledTimes(2);
cond.set(false);
expect(fn).toBeCalledTimes(3);
a.set(3);
// should not trigger fn
expect(fn).toBeCalledTimes(3);
b.set(3);
expect(fn).toBeCalledTimes(4);
});
});