From a7a57426b068c89f67fecbbaaf0d4138d927122c Mon Sep 17 00:00:00 2001 From: Hoikan <408255371@qq.com> Date: Wed, 7 Feb 2024 08:27:01 +0000 Subject: [PATCH] !139 feat(no-vdom): if-else element * feat(no-vdom): if-else element --- packages/inula-novdom/src/components/Cond.tsx | 60 +++ packages/inula-novdom/src/components/Show.tsx | 56 --- packages/inula-novdom/src/core.ts | 5 +- packages/inula-novdom/src/dom.ts | 2 +- packages/inula-novdom/src/type.ts | 1 + packages/inula-novdom/tests/For.test.tsx | 4 +- .../inula-novdom/tests/conditions.test.tsx | 408 ++++++++++++++++ packages/inula-novdom/tests/render.test.ts | 446 +----------------- packages/inula-reactive/src/RNodeCreator.ts | 9 +- packages/inula-reactive/tests/untrack.test.ts | 48 ++ 10 files changed, 530 insertions(+), 509 deletions(-) create mode 100644 packages/inula-novdom/src/components/Cond.tsx delete mode 100644 packages/inula-novdom/src/components/Show.tsx create mode 100644 packages/inula-novdom/tests/conditions.test.tsx create mode 100644 packages/inula-reactive/tests/untrack.test.ts diff --git a/packages/inula-novdom/src/components/Cond.tsx b/packages/inula-novdom/src/components/Cond.tsx new file mode 100644 index 00000000..17bb93e8 --- /dev/null +++ b/packages/inula-novdom/src/components/Cond.tsx @@ -0,0 +1,60 @@ +/* + * 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 { JSXElement } from '../type'; +import { computed, isReactiveObj, untrack } from 'inula-reactive'; + +// It's boolean when the condition is static of default branch +// Otherwise it's a function that return boolean +type CondExpression = boolean | (() => boolean); +// When branch only include static JSXElement the branch can be a JSXElement +// Otherwise, the branch should be a function that return JSXElement +type Branch = JSXElement | (() => JSXElement); + +export interface CondProps { + // Array of tuples, first item is the condition, second is the branch to render + 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) { + return branch; + } + } + }, false); + + // Compute the current branch, when condition changes or when the branch changes + return computed(() => { + let result = currentBranch.get(); + if (typeof result === 'function') { + // untrack the result to avoid reactivity. + untrack(() => { + result = result(); + // Nested condition will return a reactive object, we need to get the value + // to avoid create a new render effect in insert function. + while (isReactiveObj(result)) { + result = result.get(); + } + }); + } + return result; + }, false); +} diff --git a/packages/inula-novdom/src/components/Show.tsx b/packages/inula-novdom/src/components/Show.tsx deleted file mode 100644 index 03774066..00000000 --- a/packages/inula-novdom/src/components/Show.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2020 Huawei Technologies Co.,Ltd. - * - * openGauss 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 { isReactiveObj } from 'inula-reactive'; - -export function Show({ - if: rIf, - else: rElse, - children, -}: { - if: any | (() => T); - else?: any; - children: any; -}): any { - return () => { - const ifValue: any = calculateReactive(rIf); - - let child: any = null; - if (ifValue) { - child = typeof children === 'function' ? children() : children; - } else { - child = typeof rElse === 'function' ? rElse() : rElse; - } - - return child; - }; -} - -/** - * 如果是函数就执行,如果是reactive就调用get() - * @param val 值/reactive对象/函数 - * @return 返回真实值 - */ -export function calculateReactive(val: any | (() => any)): any { - let ret = val; - if (typeof val === 'function') { - ret = val(); - } - - if (isReactiveObj(ret)) { - ret = ret.get(); - } - - return ret; -} diff --git a/packages/inula-novdom/src/core.ts b/packages/inula-novdom/src/core.ts index 5a5d41e5..b681c09c 100644 --- a/packages/inula-novdom/src/core.ts +++ b/packages/inula-novdom/src/core.ts @@ -13,13 +13,14 @@ * See the Mulan PSL v2 for more details. */ -import { insert } from './dom'; +import {insert} from './dom'; +import { untrack } from 'inula-reactive/dist'; type ComponentConstructor = (props: T) => any; type CodeFunction = () => any; export function runComponent(Comp: ComponentConstructor, props: T = {} as T): any { - return Comp(props); + return untrack(() => Comp(props)); } export function render(codeFn: CodeFunction, element: HTMLElement): () => void { diff --git a/packages/inula-novdom/src/dom.ts b/packages/inula-novdom/src/dom.ts index 10ff619c..077b6a28 100644 --- a/packages/inula-novdom/src/dom.ts +++ b/packages/inula-novdom/src/dom.ts @@ -290,7 +290,7 @@ export function setAttribute(node: Element, name: string, value: string | null): } } -export function className(node: Element, value: string | string[] | Record | null): void { +export function className(node: Element, value: string | Record | null): void { if (value == null) { node.removeAttribute('class'); } else { diff --git a/packages/inula-novdom/src/type.ts b/packages/inula-novdom/src/type.ts index 4e06fa03..ab63f91a 100644 --- a/packages/inula-novdom/src/type.ts +++ b/packages/inula-novdom/src/type.ts @@ -14,5 +14,6 @@ */ // TODO: JSX type +export type JSXElement = any; export type FunctionComponent> = (props: Props) => unknown; export type AppDisposer = () => void; diff --git a/packages/inula-novdom/tests/For.test.tsx b/packages/inula-novdom/tests/For.test.tsx index 5a72f161..e2ea6dd1 100644 --- a/packages/inula-novdom/tests/For.test.tsx +++ b/packages/inula-novdom/tests/For.test.tsx @@ -152,8 +152,8 @@ describe('For', () => { ], }) ); - $$on(_el$8, 'click', add); - $$on(_el$10, 'click', push); + $$on(_el$8, 'click', add, true); + $$on(_el$10, 'click', push, true); return _el$5; })(); }; diff --git a/packages/inula-novdom/tests/conditions.test.tsx b/packages/inula-novdom/tests/conditions.test.tsx new file mode 100644 index 00000000..6845efc8 --- /dev/null +++ b/packages/inula-novdom/tests/conditions.test.tsx @@ -0,0 +1,408 @@ +/* + * 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 } from '../src/core'; +import { reactive } from 'inula-reactive'; + +describe('conditions', () => { + it('should render the first branch that matches the condition.', ({ container }) => { + /** + * 源码: + * const fn = vi.fn(); + * function App() { + * const x = reactive(7); + * + * return ( + *
+ *

if

+ * 10}> + *

{x.get()} is greater than 10

+ *
+ * x.get()}> + *

{x.get()} is less than 5

+ *
+ * + *

{x.get()} is between 5 and 10

+ *
+ *
+ * ); + * } + * render(() => , container); + */ + + // 编译后: + + const _tmpl$ = /*#__PURE__*/ _$template(`

is greater than 10`), + _tmpl$2 = /*#__PURE__*/ _$template(`

is less than 5`), + _tmpl$3 = /*#__PURE__*/ _$template(`

xxx`), + _tmpl$4 = /*#__PURE__*/ _$template(`

is between 5 and 10`); + let change; + + function App() { + const x = reactive(7); + change = v => x.set(v); + return (() => { + const _el$ = _tmpl$3(), + _el$2 = _el$.firstChild; + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + _$insert(_el$3, x, _el$4); + return _el$3; + }, + ], + [ + () => 5 > x.get(), + () => { + const _el$5 = _tmpl$2(), + _el$6 = _el$5.firstChild; + _$insert(_el$5, x, _el$6); + return _el$5; + }, + ], + [ + true, + () => { + const _el$7 = _tmpl$4(), + _el$8 = _el$7.firstChild; + _$insert(_el$7, x, _el$8); + return _el$7; + }, + ], + ]; + }, + }), + null + ); + + return _el$; + })(); + } + + render(() => _$runComponent(App, {}), container); + + expect(container.innerHTML).toBe('

xxx

7 is between 5 and 10

'); + change(11); + expect(container.innerHTML).toBe('

xxx

11 is greater than 10

'); + change(4); + expect(container.innerHTML).toBe('

xxx

4 is less than 5

'); + }); + + it('should not render any branch if all conditions failed', ({ container }) => { + /** + * 源码: + * const fn = vi.fn(); + * function App() { + * const x = reactive(7); + * + * return ( + *
+ *

xxx

+ * 10}> + *

{x.get()} is greater than 10

+ *
+ *

xxx

+ *
+ * ); + * } + * render(() => , container); + */ + + // 编译后: + + const _tmpl$ = /*#__PURE__*/ _$template(`

is greater than 10`), + _tmpl$3 = /*#__PURE__*/ _$template(`

xxx`); + let change; + + function App() { + const x = reactive(7); + change = v => x.set(v); + return (() => { + const _el$ = _tmpl$3(), + _el$2 = _el$.firstChild; + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + _$insert(_el$3, x, _el$4); + return _el$3; + }, + ], + ]; + }, + }), + null + ); + + return _el$; + })(); + } + + render(() => _$runComponent(App, {}), container); + + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

"'); + change(11); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

11 is greater than 10

"'); + }); + + it('should render nested conditions', ({ container }) => { + /** + * 源码: + * const fn = vi.fn(); + * function App() { + * const x = reactive(7); + * + * return ( + *
+ *

xxx

+ * 10}> + *

{x.get()} is greater than 10

+ *
+ * x.get()}> + *

{x.get()} is less than 5

+ *
+ * + * 7}> + *

{x.get()} is greater than 7

+ *
+ * + *

{x.get()} is 7 or less

+ *
+ *
+ *
+ * ); + * } + * render(() => , container); + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ _$template(`

is greater than 10`), + _tmpl$2 = /*#__PURE__*/ _$template(`

is less than 5`), + _tmpl$3 = /*#__PURE__*/ _$template(`

is greater than 7`), + _tmpl$4 = /*#__PURE__*/ _$template(`

is 7 or less`), + _tmpl$5 = /*#__PURE__*/ _$template(`

xxx`); + let change; + + function App() { + const x = reactive(7); + change = v => x.set(v); + return (() => { + const _el$ = _tmpl$5(), + _el$2 = _el$.firstChild; + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => x.get() > 10, + () => { + const _el$3 = _tmpl$(), + _el$4 = _el$3.firstChild; + _$insert(_el$3, x, _el$4); + return _el$3; + }, + ], + [ + () => 5 > x.get(), + () => { + const _el$5 = _tmpl$2(), + _el$6 = _el$5.firstChild; + _$insert(_el$5, x, _el$6); + return _el$5; + }, + ], + [ + true, + () => { + return _$runComponent(Cond, { + get branches() { + return [ + [ + () => x.get() > 7, + () => { + const _el$8 = _tmpl$3(), + _el$9 = _el$8.firstChild; + _$insert(_el$8, x, _el$9); + return _el$8; + }, + ], + [ + true, + () => { + const _el$10 = _tmpl$4(), + _el$11 = _el$10.firstChild; + _$insert(_el$10, x, _el$11); + return _el$10; + }, + ], + ]; + }, + }); + }, + ], + ]; + }, + }), + null + ); + return _el$; + })(); + } + + render(() => _$runComponent(App, {}), container); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

7 is 7 or less

"'); + change(11); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

11 is greater than 10

"'); + change(4); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

4 is less than 5

"'); + change(8); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

8 is greater than 7

"'); + change(6); + expect(container.innerHTML).toMatchInlineSnapshot('"

xxx

6 is 7 or less

"'); + }); + + it('should render parallel conditions', ({ container }) => { + /** + * 源码: + * const fn = vi.fn(); + * function App() { + * const showX = reactive(true); + * const showY = reactive(true); + * const showZ = reactive(true); + * return ( + *
+ *

parallel

+ * + *

XXX

+ *
+ * + *

YYY

+ *
+ * + *

ZZZ

+ *
+ *
+ * ); + * } + * render(() => , container); + */ + + // 编译后: + + const _tmpl$ = /*#__PURE__*/ _$template(`

XXX`), + _tmpl2$ = /*#__PURE__*/ _$template(`

YYY`), + _tmpl3$ = /*#__PURE__*/ _$template(`

ZZZ`), + _tmpl$4 = /*#__PURE__*/ _$template(`

parallel`); + + const showX = reactive(true); + const showY = reactive(true); + const showZ = reactive(true); + + function App() { + return (() => { + const _el$ = _tmpl$4(), + _el$2 = _el$.firstChild; + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => showX.get(), + () => { + return _tmpl$(); + }, + ], + ]; + }, + }), + null + ); + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => showY.get(), + () => { + return _tmpl2$(); + }, + ], + ]; + }, + }), + null + ); + _$insert( + _el$, + _$runComponent(Cond, { + get branches() { + return [ + [ + () => showZ.get(), + () => { + return _tmpl3$(); + }, + ], + ]; + }, + }), + null + ); + + return _el$; + })(); + } + + render(() => _$runComponent(App, {}), container); + + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

XXX

YYY

ZZZ

"'); + // hide X, Y, Z randomly + showY.set(false); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

XXX

ZZZ

"'); + showX.set(false); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

ZZZ

"'); + showZ.set(false); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

"'); + // show X, Y, Z randomly + showY.set(true); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

YYY

"'); + showZ.set(true); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

YYY

ZZZ

"'); + showX.set(true); + expect(container.innerHTML).toMatchInlineSnapshot('"

parallel

XXX

YYY

ZZZ

"'); + }); +}); diff --git a/packages/inula-novdom/tests/render.test.ts b/packages/inula-novdom/tests/render.test.ts index 5f85efaa..5bd3ed6e 100644 --- a/packages/inula-novdom/tests/render.test.ts +++ b/packages/inula-novdom/tests/render.test.ts @@ -14,11 +14,10 @@ */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck For the compiled code. -import { computed, reactive, watch } from 'inula-reactive'; +import { reactive } from 'inula-reactive'; import { template as $$template, insert as $$insert, - setAttribute as $$setAttribute, effect as $$effect, style as $$style, className as $$className, @@ -27,8 +26,6 @@ import { runComponent as $$runComponent, render } from '../src/core'; import { delegateEvents as $$delegateEvents, addEventListener as $$on } from '../src/event'; import { describe, expect } from 'vitest'; import { domTest as it } from './utils'; -import { Show } from '../src/components/Show'; -import { For } from '../src/components/For'; describe('render', () => { it('should render plain jsx', ({ container }) => { @@ -553,445 +550,4 @@ describe('render', () => { render(() => $$runComponent(Comp, {}), container); expect(container.querySelector('div').className).toEqual('red green'); }); - - it('使用Show组件', ({ container }) => { - /** - * 源码: - * const CountValue = (props) => { - * return
Count value is {props.count()}.
; - * } - * - * const CountingComponent = () => { - * const [count, setCount] = createSignal(0); - * const add = () => { - * setCount((c) => c + 1); - * } - * - * return
- * 0} fallback={}> - * - * - *
- *
; - * }; - * - * render(() => , document.getElementById("app")); - */ - - // 编译后: - const $tmpl = /*#__PURE__*/ $$template('
Count value is .'), - $tmpl_2 = /*#__PURE__*/ $$template('
- *
- *
; - * }; - * - * render(() => , document.getElementById("app")); - */ - - // 编译后: - const $tmpl = /*#__PURE__*/ $$template('
Count value is .'), - $tmpl_2 = /*#__PURE__*/ $$template( - '
- *
- * ); - * - * const Main = () => { - * const [state, setState] = createStore({data: [{id: 1, label: '111', selected: false}, {id: 2, label: '222', selected: false}], num: 2}); - * - * function run() { - * setState('data', buildData(5)); - * } - * - * return ( - *
- *
- *
- *

Horizon-reactive-novnode

- *
- *
- *
- *
- *
- *
- * - * - *
- *
- * ); - * }; - * - * render(() =>
, document.getElementById("app")); - */ - - // 编译后: - const $tmpl = /*#__PURE__*/ $$template(''), - $tmpl_2 = /*#__PURE__*/ $$template('