parent
80f3be9436
commit
521344f8ff
|
@ -24,15 +24,13 @@ type Branch = JSXElement | (() => JSXElement);
|
||||||
|
|
||||||
export interface CondProps {
|
export interface CondProps {
|
||||||
// Array of tuples, first item is the condition, second is the branch to render
|
// 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) {
|
export function Cond(props: CondProps) {
|
||||||
// Find the first branch that matches the condition
|
// Find the first branch that matches the condition
|
||||||
// Any signal that used in condition expression, will trigger the condition to recompute
|
// Any signal that used in condition expression, will trigger the condition to recompute
|
||||||
const currentBranch = computed(() => {
|
const currentBranch = computed(() => {
|
||||||
// clean up the previous branch
|
|
||||||
|
|
||||||
for (let i = 0; i < props.branches.length; i++) {
|
for (let i = 0; i < props.branches.length; i++) {
|
||||||
const [condition, branch] = props.branches[i];
|
const [condition, branch] = props.branches[i];
|
||||||
if (typeof condition === 'function' ? condition() : condition) {
|
if (typeof condition === 'function' ? condition() : condition) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -15,12 +15,16 @@
|
||||||
|
|
||||||
import { insert } from './dom';
|
import { insert } from './dom';
|
||||||
import { untrack } from 'inula-reactive';
|
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;
|
type CodeFunction = () => any;
|
||||||
|
|
||||||
export function runComponent<T>(Comp: ComponentConstructor<T>, props: T = {} as T): 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;
|
const ELEMENT_NODE = 1;
|
||||||
|
|
|
@ -15,5 +15,8 @@
|
||||||
|
|
||||||
// TODO: JSX type
|
// TODO: JSX type
|
||||||
export type JSXElement = any;
|
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 AppDisposer = () => void;
|
||||||
|
export type Props = Record<string, unknown>;
|
||||||
|
export type Context = Props;
|
||||||
|
export type WithChildren<T = Props> = T & { children?: JSXElement };
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -49,7 +49,7 @@ describe('render', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 0.');
|
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 = () => {
|
* const CountingComponent = () => {
|
||||||
|
@ -123,7 +123,7 @@ describe('render', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render sub components', ({ container }) => {
|
it('should render components', ({ container }) => {
|
||||||
/**
|
/**
|
||||||
* 源码:
|
* 源码:
|
||||||
* const CountValue = (props) => {
|
* const CountValue = (props) => {
|
||||||
|
@ -185,7 +185,81 @@ describe('render', () => {
|
||||||
expect(container.querySelector('#count').innerHTML).toEqual('Count value is 1<!---->.');
|
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) => {
|
* const CountValue = (props) => {
|
||||||
|
|
|
@ -306,7 +306,7 @@ export function runEffects(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不进行响应式数据的使用追踪
|
// 不进行响应式数据的使用追踪
|
||||||
export function untrack(fn: () => void) {
|
export function untrack<T>(fn: () => T): T {
|
||||||
if (runningRNode === null) {
|
if (runningRNode === null) {
|
||||||
return fn();
|
return fn();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ import { getRNodeVal } from './RNodeAccessor';
|
||||||
export function createReactive<T extends string>(raw?: T): Signal<string>;
|
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 number>(raw?: T): Signal<number>;
|
||||||
export function createReactive<T extends symbol>(raw?: T): Signal<symbol>;
|
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 Record<any, any> | Array<any> | symbol>(raw?: T): DeepReactive<T>;
|
||||||
export function createReactive<T extends NonFunctionType>(raw: T, deep = true): DeepReactive<T> | Signal<T> {
|
export function createReactive<T extends NonFunctionType>(raw: T, deep = true): DeepReactive<T> | Signal<T> {
|
||||||
// Function, Date, RegExp, null, undefined are simple signals
|
// Function, Date, RegExp, null, undefined are simple signals
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the Mulan PSL v2 for more details.
|
* 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 { isReactiveObj } from './Utils';
|
||||||
import { RNode, untrack, runEffects } from './RNode';
|
import { RNode, untrack, runEffects } from './RNode';
|
||||||
import { setScheduler } from './SetScheduler';
|
import { setScheduler } from './SetScheduler';
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue