diff --git a/packages/inula-novdom/src/components/Dynamic.tsx b/packages/inula-novdom/src/components/Dynamic.tsx new file mode 100644 index 00000000..bbffd0ec --- /dev/null +++ b/packages/inula-novdom/src/components/Dynamic.tsx @@ -0,0 +1,45 @@ +/* + * 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 { FunctionComponent, Props } from '../type'; +import { insert } from '../dom'; +import { splitProps } from '../util'; + +type DynamicProps

= { + // it can be a native element tag name or a component + component: string | FunctionComponent

; +} & P; + +/** + * Dynamic component that can be used to switch between different components or native elements + * @example + * const comp = reactive('h1'); + * Title + */ +export function Dynamic(props: DynamicProps) { + return () => { + if (typeof props.component === 'string') { + const el = document.createElement(props.component); + insert(el, props.children); + return el; + } else if (typeof props.component === 'function') { + // remove component from props, and pass the rest to the component + const [, compProps] = splitProps(props, ['component']); + return props.component(compProps); + } else { + throw new Error('Invalid component for Dynamic'); + } + }; +} diff --git a/packages/inula-novdom/tests/Dynamic.test.tsx b/packages/inula-novdom/tests/Dynamic.test.tsx new file mode 100644 index 00000000..9fc6f5c6 --- /dev/null +++ b/packages/inula-novdom/tests/Dynamic.test.tsx @@ -0,0 +1,183 @@ +/* + * 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 { runComponent as $$runComponent, render } from '../src/core'; +import { Dynamic } from '../src/components/Dynamic'; +import { reactive } from 'inula-reactive'; + +describe('Dynamic', () => { + it('should work with native elements.', ({ container }) => { + /** + * 源码: + * function App() { + * return foo; + * } + * render(() => , container); + */ + + // 编译后: + function App() { + return $$runComponent(Dynamic, { + component: 'h1', + children: 'foo', + }); + } + + render(() => $$runComponent(App, {}), container); + expect(container).toMatchInlineSnapshot(` +

+

+ foo +

+
+ `); + }); + + it('should work with components.', ({ container }) => { + /** + * 源码: + * function App() { + * return ; + * } + * function Title(props) { + * return

{props.name}

; + * } + * render(() => , container); + */ + + // 编译后: + function App() { + return $$runComponent(Dynamic, { + component: Title, + name: 'bar', + }); + } + + const _tmpl = /*#__PURE__*/ $$template('

'); + + function Title(props) { + return (() => { + const _el$ = _tmpl(); + $$insert(_el$, () => props.name, null); + return _el$; + })(); + } + + render(() => $$runComponent(App, {}), container); + expect(container).toMatchInlineSnapshot(` +
+

+ bar +

+
+ `); + }); + + it('should throw on invalid component.', ({ container }) => { + /** + * 源码: + * function App() { + * return ; + * } + * render(() => , container); + */ + + // 编译后: + function App() { + return $$runComponent(Dynamic, { + component: null, + }); + } + + expect(() => render(() => $$runComponent(App, {}), container)).toThrowError('Invalid component for Dynamic'); + }); + + it('should change component.', async ({ container }) => { + /** + * 源码: + * const H1 = (props) =>

{props.children}

; + * const H3 = (props) =>

{props.children}

; + * function App() { + * const comp = reactive('h1'); + * const comps = { + * H1, + * H3, + * h1: 'h1', + * h2: 'h2', + * } + * return ( + *
+ * foo + *
+ * ); + * } + * render(() => , container); + * + */ + + // 编译后: + const _tmpl$ = /*#__PURE__*/ $$template('
'); + const _h1 = /*#__PURE__*/ $$template('

'), + _h3 = /*#__PURE__*/ $$template('

'); + const H1 = (props: { children: any }) => { + const _el$ = _h1(); + $$insert(_el$, () => props.children); + return _el$; + }; + const H3 = (props: { children: any }) => { + const _el$3 = _h3(); + $$insert(_el$3, () => props.children); + return _el$3; + }; + + const comp = reactive('h1'); + + function App() { + const comps = { + H1, + H3, + h1: 'h1', + h2: 'h2', + }; + return (() => { + const _div = _tmpl$(); + $$insert( + _div, + $$runComponent(Dynamic, { + get component() { + return comps[comp.get()]; + }, + children: 'foo', + }) + ); + return _div; + })(); + } + + render(() => $$runComponent(App, {}), container); + expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); + comp.set('h2'); + expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); + comp.set('H3'); + expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); + comp.set('H1'); + expect(container.innerHTML).toMatchInlineSnapshot('"

foo

"'); + }); +});