;
+}
+```
+
+```js
+function MyComp() {
+ let prop1$$prop
+
+ let count = 1
+ let doubleCount = count * 2
+ let update$$doubleCount = () => {
+ if (cached()) return
+ doubleCount = count * 2
+ }
+
+ const updateCount = () => {
+ viewModel.update(count++)
+ }
+
+ // ----
+ const node = createElement('div')
+ setProps(node, "textContent", [prop1])
+
+ const viewModel = Inula.createElement({
+ node,
+ updateProp: (propName, value) => {
+ if (propName === 'prop1') {
+ prop1$$prop = value
+ }
+ },
+ updateState: (changed) => {
+ if (changed & 0x1) {
+ update$$doubleCount()
+ }
+ },
+ updateView: (changed) => {
+ if (changed & 0x1) {
+ setProps(node, "textContent", [prop1$$prop])
+ }
+ }
+ })
+
+ return viewModel
+}
+```
+
diff --git a/api-2.1.md b/api-2.1.md
new file mode 100644
index 00000000..b5c51ac2
--- /dev/null
+++ b/api-2.1.md
@@ -0,0 +1,2029 @@
+# Identify a Component/hook
+## 1. Auto detection config turned on in Babel
+```json
+{
+ "autoNamingDetection": true
+}
+```
+Will follow the following rules:
+* For Components:
+ 1. Inside a .jsx file
+ 2. Function/ArrowFunction Named as PascalCase
+ ```jsx
+ // ✅
+ function MyComp() {
+ return (
+
+
Hello
+
+ )
+ }
+ // ❌
+ function foo() {
+ return (
+
+
Hello
+
+ )
+ }
+ ```
+* For hooks:
+ 1. Function/ArrowFunction starts with "use"
+ ```jsx
+ // ✅
+ function useMyHook() {
+ return 1
+ }
+ // ❌
+ function foo() {
+ return 2
+ ```
+## 2. Manual detection
+No naming convention is required. The user will have to manually wrap the function with `Component` or `Hook` function.
+
+* For Components, wrapped with `Component` function
+```jsx
+const myComp = Component(function() {
+ return (
+
+
Hello
+
+ )
+})
+```
+* For hooks, wrapped with `Hook` function
+```jsx
+const myHook = Hook(function() {
+ return 1
+})
+```
+
+
+# States/Computed
+```jsx
+function MyComp() {
+ let a, b
+ let count = 100
+ let doubleCount = count * 2
+ let [a1, a2, a3] = getArray()
+ ...
+}
+
+```
+
+```jsx
+class MyComp extends View {
+ // Multiple declarations in one line
+ a
+ b
+ // Single declaration
+ count = 100
+ // Computed states
+ doubleCount = this.count * 2
+
+ // Deconstructing assignment
+ a1
+ a2
+ a3
+ // Using @Watch to deconstruct
+ @Watch
+ _$deconstruct_$id$() {
+ [this.a1, this.a2, this.a3] = getArray()
+ }
+}
+```
+
+# Props
+
+## Step 1: Collect props/destructured/restProps
+### Deconstructed in the function signature
+```jsx
+function MyComp({ prop1, prop2: [p20, p21], ...otherProps }) {
+ ...
+}
+```
+Collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2": astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+### Deconstructing assignment / Direct usage in the function body
+#### 1. Deconstructing assignment
+```jsx
+function MyComp(props) {
+ const { prop1, prop2: [p20, p21], ...otherProps } = props
+ ...
+}
+```
+Collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+After:
+* Delete this statement
+
+#### 2. Direct usage
+```jsx
+function MyComp(props) {
+ let prop1 = props.prop1
+ let [p20, p21] = props.prop2
+ // Not just assignment, but also any other type of usage
+ console.log(props.prop3)
+ ...
+}
+```
+collected as:
+```js
+props = ["prop1", "prop2", "prop3"]
+destructured = [] // The reason why it's empty is that the deconstructed props are treated like Computed States
+restProps = null
+```
+After:
+* Replace all `props.propName` with `propName`
+
+Goals of these two "After"s:
+* Make sure no `props` is used in the function body
+
+## Step2: Generate Props in class
+With the collected props/destructured/restProps:
+```jsx
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+]
+restProps = "otherProps"
+```
+Generate:
+```jsx
+class MyComp extends View {
+ // Props with @Prop decorator
+ @Prop prop1
+ @Prop prop2
+
+ // Destructured props, same logic as Computed States
+ p20
+ p21
+ @Watch
+ _$deconstruct_$id$() {
+ [this.p20, this.p21] = this.prop2
+ }
+
+ // Rest props
+ @RestProps otherProps
+}
+```
+
+## Things to note
+1. Multiple deconstructed statements are allowed:
+```jsx
+function MyComp(props) {
+ const { prop1, prop2: [p20, p21] } = props
+ const { prop1: p1, prop2: [p20A, p21A] } = props
+ ...
+}
+```
+will be collected as:
+```js
+props = ["prop1", "prop2"]
+destructured = [
+ ["prop2", astOf("[p20, p21]")],
+ ["prop2", astOf("[p20A, p21A)")]
+] // this is why destructured is an array of arrays instead of an object
+```
+and be generated into:
+```jsx
+class MyComp extends View {
+ @Prop prop1
+ @Prop prop2
+
+ p20
+ p21
+ @Watch
+ _$deconstruct_$id1$() {
+ [this.p20, this.p21] = this.prop2
+ }
+
+ p20A
+ p21A
+ @Watch
+ _$deconstruct_$id2$() {
+ [this.p20A, this.p21A] = this.prop2
+ }
+}
+```
+
+2. Multiple restProps are NOT allowed:
+```jsx
+function MyComp(props) {
+ const { prop1, prop2, ...otherProps1 } = props
+ const { prop2, prop3, ...otherProps2 } = props
+ ...
+}
+```
+Will throw an error because this will confuse the compiler what props are explicitly used and what are collected as restProps.
+
+3. Anywhere that `props` is used in the function body will be replaced with the prop name directly:
+```jsx
+function MyComp(props) {
+ let p1 = props.prop1
+ console.log(props.prop2)
+ console.log(props.prop3.xx[0].yy)
+ ...
+}
+```
+will be replaced as:
+```jsx
+class MyComp extends View {
+ @Prop prop1
+ @Prop prop2
+ @Prop prop3
+
+ p1 = this.prop1
+
+ willMount() {
+ console.log(this.prop2)
+ console.log(this.prop3.xx[0].yy)
+ }
+}
+```
+
+# For loops
+`for` tag basic usage (just like solid's For):
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
)}
+ >
+ )
+}
+```
+when it's detected as a `map` function and the returned value is a JSX, it will be converted into a `for` tag(for better performance):
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+NOTE: only detect the last map function:
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ <>
+ {
+ Object.values(arr)
+ .map(item =>
+ }
+ )
+}
+```
+
+### 2. Children
+1. Simple returned value (no local variables). Children will be treated as regular elements/components
+```jsx
+function MyComp() {
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+children collected in the parser will only be:
+```jsx
+
{item}
+```
+
+2. With local variables. Transformed into a new Component:
+```jsx
+function MyComp() {
+ return (
+ {(info) => {
+ const { item } = info
+ return
{item}
+ }}
+ )
+}
+```
+will be converted into:
+```jsx
+function MyComp() {
+ function Comp_$id$({ info }) {
+ const { item } = info
+ return
{item}
+ }
+
+ return (
+ {(info) => (
+
+ )}
+ )
+}
+```
+which creates a new component for the children and converts the children into the first situation(1. Simple returned value)
+
+## Key prop
+Just like React, we can add a `key` prop to first level children of the `for` tag:
+```jsx
+function MyComp() {
+ let arr = [1, 2, 3]
+ return (
+ {(item) =>
+
{item}
+ }
+ )
+}
+```
+
+
+# JSX
+JSX is tricky because we need to allow different types of JSX to be used in the function body.
+
+Let's consider a piece of JSX:
+```jsx
+function MyComp() {
+ let count = 0
+ let jsxSlice =
{count}
+ ...
+}
+```
+We have different options:
+1. jsx -> array of nodes
+
+we can use an IIFE to convert the JSX into an array of nodes:
+```jsx
+class MyComp extends View {
+ count = 0
+ jsxSlice = (() => {
+ const node0 = createElement("div")
+ node0.textContent = this.count
+ return node0
+ })()
+}
+```
+This looks good but when `count` changes, the whole `jsxSlice` will be re-calculated, which means elements will be re-created. This is not good for performance.
+
+So instead of using an IIFE, we can first extract the JSX into a nested component:
+```jsx
+function MyComp() {
+ let count = 0
+ function Comp_$id$() {
+ return
{count}
+ }
+ let jsxSlice =
+ ...
+}
+```
+In this case, no matter how many times `count` changes, the `jsxSlice` will not be re-calculated, we only need to call the update function of the `Comp_$id$` component.
+
+So we'll convert this type of JSX into a nested component:
+```jsx
+class MyComp extends View {
+ count = 0
+ // Use an IIFE to return a class because we need to forward the parent's this. X in _$thisX represents the level of nesting
+ // Also it needs to be static(not a state variable)
+ @Static Comp_$id$ = (() => {
+ const _$this0 = this
+ return class extends View {
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(_$this0.count)
+ appendChild(node0, [node1])
+
+ const update = changed => {
+ if (changed & 0x0001) {
+ node1.update(_$this0.count)
+ }
+ }
+ // Because we need to confine the changed scope, we put update into all parent level's updates
+ this._$updates.add(update) // current level
+ _$this0._$updates.add(update) // parent level
+ // _$this1._$updates.add(update) // if there's a third level
+ return node0
+ }
+ }
+ })()
+
+ jsxSlice = new this.Comp_$id$()
+}
+```
+
+A more complex example:
+```jsx
+function MyComp() {
+ let count = 100
+ const jsxSlice =
{count}
+ const jsxArray = [
1
,
{count}
]
+ function jsxFunc() {
+ // This is a function that returns JSX
+ // because the function name is smallCamelCased
+ return
+ }
+ const jsxArray = [, ]
+ function jsxFunc() {
+ function Comp_$id4$() {
+ return
{count}
+ }
+ // This is a function that returns JSX
+ // because the function name is smallCamelCased
+ return
+ }
+ function InternalComp({ doubleCount }) {
+ return (
+
+ {count /* This is Parent's state */}
+ {doubleCount /* This is current's prop */}
+
+ )
+ }
+
+ return (
+
+ {jsxSlice}
+ {jsxArray}
+ {jsxFunc()}
+
+
+ )
+}
+```
+will be converted into:
+```jsx
+class MyComp extends View {
+ count = 100
+ // $$count = 0x0001 // Indicate count is the first state
+
+ @Static Comp_$id1$ = (() => {
+ const _$this0 = this
+ return class extends View {
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(_$this0.count)
+ appendChild(node0, [node1])
+ const update = changed => {
+ if (changed & 0x0001) {
+ node1.update(_$this0.count)
+ }
+ }
+ this._$updates.add(update)
+ _$this0._$updates.add(update)
+ return node0
+ }
+ }
+ })()
+ jsxSlice = new this.Comp_$id$(null, [...this._$scopes, this])
+
+ @Static Comp_$id2$ = class extends View {
+ Body() {
+ const node0 = createElement("div")
+ node0.textContent = "1"
+ return node0
+ }
+ }
+ Comp_$id3$ = /*same as Comp_$id1$*/
+ jsxArray = [new this.Comp_$id2$(), new this.Comp_$id3$()]
+
+ jsxFunc() {
+ const Comp_$id4$ = /*same as Comp_$id1$*/
+ return new Comp_$id4$()
+ }
+ @Static InternalComp = (() => {
+ const _$this0 = this
+ return class extends View {
+ @Prop doubleCount
+ // $$doubleCount = 0x0010 // Index will be INHERITED from the parent!
+ willUnmount() {
+ // Remove all parent level when unmount
+ clear(_$this0)
+ }
+ Body() {
+ const node1 = createElement("div")
+ const node2 = new ExpressionNode(_$this0.count)
+ const node3 = new ExpressionNode(this.doubleCount)
+ appendChild(node1, [node2, node3])
+ const update = changed => {
+ if (changed & 0x0001) {
+ node2.update(_$this0.count)
+ }
+ if (changed & 0x0010) {
+ node3.update(this.doubleCount)
+ }
+ }
+ this._$updates.add(update)
+ _$this0._$updates.add(update)
+ return [node1]
+ }
+ }
+ })()
+
+ Body() {
+ const node0 = createElement("div")
+ const node1 = new ExpressionNode(this.jsxSlice)
+ const node2 = new ExpressionNode(this.jsxArray)
+ const node3 = new ExpressionNode(this.jsxFunc())
+ const node4 = new this.InternalComp({
+ doubleCount: this.count * 2
+ })
+ // second argument is the parent scopes, use this to access parent's states/variables
+
+ appendChild(node0, [node1, node2, node3, node4])
+ const update = changed => {
+ if (changed & 0x0001) {
+ // _$this0.jsxFunc will be re-called
+ node3.update(this.jsxFunc(this.count))
+ // doubleCount prop will be updated due to count change
+ node4.updateProp("doubleCount", this.count * 2)
+ }
+ }
+ this._$updates.add(update)
+ return node0
+ }
+}
+```
+
+# Early return
+Early return is also tricky....
+
+```jsx
+function MyComp() {
+ let count = 100
+ if (count === 100) {
+ return
100
+ }
+ let flag = true
+ return
Not 100 {flag}
+}
+```
+it will be converted in a functional level:
+```jsx
+function MyComp() {
+ let count = 100
+ function Comp_$id1$() {
+ return
100
+ }
+ function Comp_$id2$() {
+ let flag = true
+ return
Not 100 {flag}
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ >
+ )
+}
+```
+and then be converted into class using the same jsx logic as the previous section.
+
+A more complex example:
+```jsx
+function MyComp() {
+ let count = 100
+ if (count === 100) {
+ return
100
+ }
+ let flag = true
+ if (flag) {
+ let cc = 0
+ if (cc === 200) {
+ return
cc is 200
+ } else if (cc === 100) {
+ return
cc is 100
+ }
+ return
Flag is true
+ }
+ return
Not 100 {flag}
+}
+```
+will be converted into:
+```jsx
+function MyComp() {
+ let count = 100
+ function Comp_$id1$() {
+ return
100
+ }
+ function Comp_$id2$() {
+ let flag = true
+ function Comp_$id3$() {
+ let cc = 0
+ function Comp_$id4$() {
+ return
)
+ }
+}
+```
+Basically turn `useMyHook(props)` into a property of `use(useMyHook, props)._$return`. The logic for props handling is the same as the Component.
+
+NOTE: We have a constraint here that we only accept `an object of props` like we do in the `Component` function.
+
+Another example:
+```jsx
+function useMyHook() {
+ let count = 100
+ let doubleCount = count * 2
+
+ return { count, doubleCount }
+}
+```
+will be converted into:
+```jsx
+class MyHook extends Model {
+ count = 100
+ doubleCount = this.count * 2
+
+ _$return = { count: this.count, doubleCount: this.doubleCount }
+}
+```
+
+used in a component:
+```jsx
+function MyComp() {
+ const { count, doubleCount } = useMyHook()
+ return
);
}
-render(MyComp, 'main');
+
+render(App, document.getElementById('main'));
diff --git a/demos/v2/src/example/attributeBinding.jsx b/demos/v2/src/example/attributeBinding.jsx
new file mode 100644
index 00000000..d4aec129
--- /dev/null
+++ b/demos/v2/src/example/attributeBinding.jsx
@@ -0,0 +1,30 @@
+/*
+ * 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 { render } from '@openinula/next';
+function AttributeBinding() {
+ let color = 'green';
+ function changeColor() {
+ color = color === 'red' ? 'green' : 'red';
+ }
+
+ return (
+
+ Click me to change color.
+
+ );
+}
+
+render(AttributeBinding, document.getElementById('app'));
diff --git a/demos/v2/src/example/condition.jsx b/demos/v2/src/example/condition.jsx
new file mode 100644
index 00000000..81a214cd
--- /dev/null
+++ b/demos/v2/src/example/condition.jsx
@@ -0,0 +1,44 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+function TrafficLight() {
+ let lightIndex = 0;
+ const TRAFFIC_LIGHTS = ['red', 'green'];
+ let light = TRAFFIC_LIGHTS[lightIndex];
+
+ function nextLight() {
+ lightIndex = (lightIndex + 1) % 2;
+ }
+
+ return (
+ <>
+
+
Light is: {light}
+
+ You must
+
+ STOP
+
+
+ GO
+
+
+ >
+ );
+}
+
+render(TrafficLight, document.getElementById('app'));
diff --git a/demos/v2/src/example/formBinding.jsx b/demos/v2/src/example/formBinding.jsx
new file mode 100644
index 00000000..e897c16e
--- /dev/null
+++ b/demos/v2/src/example/formBinding.jsx
@@ -0,0 +1,75 @@
+/*
+ * 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 { render } from '@openinula/next';
+function Form() {
+ let text = 'a';
+ let checked = true;
+ let picked = 'One';
+ let selected = 'A';
+ let multiSelected = [];
+ function updateValue(e) {
+ text = e.target.value;
+ }
+ function handleCheckboxChange(e) {
+ checked = e.target.checked;
+ }
+ function handleRadioChange(e) {
+ picked = e.target.value;
+ }
+ function handleSelectChange(e) {
+ selected = e.target.value;
+ }
+ function handleMultiSelectChange(e) {
+ multiSelected = Array.from(e.target.selectedOptions).map(option => option.value);
+ }
+ return (
+ <>
+
Text Input
+
+
{text}
+
+
Checkbox
+
+
+
+
Radio
+
+
+
+
+
+
Picked: {picked}
+
+
Select
+
+
Selected: {selected}
+
+
Multi Select
+
+
Selected: {multiSelected}
+ >
+ );
+}
+
+render(Form, document.getElementById('app'));
diff --git a/demos/v2/src/example/getData.jsx b/demos/v2/src/example/getData.jsx
new file mode 100644
index 00000000..08a44c29
--- /dev/null
+++ b/demos/v2/src/example/getData.jsx
@@ -0,0 +1,32 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+const API_URL = 'https://jsonplaceholder.typicode.com/users';
+const users = await (await fetch(API_URL)).json();
+function List({ arr }) {
+ return (
+
+ {item =>
{item.name}
}
+
+ );
+}
+
+function App() {
+ return ;
+}
+
+render(App, document.getElementById('app'));
diff --git a/packages/transpiler/babel-inula-next-core/test/presets.ts b/demos/v2/src/example/helloWorld.jsx
similarity index 70%
rename from packages/transpiler/babel-inula-next-core/test/presets.ts
rename to demos/v2/src/example/helloWorld.jsx
index fce4800d..e318d838 100644
--- a/packages/transpiler/babel-inula-next-core/test/presets.ts
+++ b/demos/v2/src/example/helloWorld.jsx
@@ -13,12 +13,9 @@
* See the Mulan PSL v2 for more details.
*/
-import plugin from '../dist';
-import { transform as transformWithBabel } from '@babel/core';
-
-export function transform(code: string) {
- return transformWithBabel(code, {
- presets: [plugin],
- filename: 'test.tsx',
- })?.code;
+import { render } from '@openinula/next';
+function HelloWorld() {
+ return
Hello World!
;
}
+
+render(HelloWorld, document.getElementById('app'));
diff --git a/demos/v2/src/example/loop.jsx b/demos/v2/src/example/loop.jsx
new file mode 100644
index 00000000..caefc1d3
--- /dev/null
+++ b/demos/v2/src/example/loop.jsx
@@ -0,0 +1,26 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+function Colors() {
+ const colors = ['red', 'green', 'blue'];
+ return (
+
+ {color =>
{color}
}
+
+ );
+}
+render(Colors, document.getElementById('app'));
diff --git a/demos/v2/src/example/simpleComponent.jsx b/demos/v2/src/example/simpleComponent.jsx
new file mode 100644
index 00000000..2cac4c26
--- /dev/null
+++ b/demos/v2/src/example/simpleComponent.jsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { render } from '@openinula/next';
+
+let fruits = ['apple', 'banana', 'pear'];
+function List({ arr }) {
+ return (
+
+ {item =>
{item}
}
+
+ );
+}
+
+function App() {
+ return ;
+}
+
+render(App, document.getElementById('app'));
diff --git a/demos/v2/src/example/userInput.jsx b/demos/v2/src/example/userInput.jsx
new file mode 100644
index 00000000..873a6201
--- /dev/null
+++ b/demos/v2/src/example/userInput.jsx
@@ -0,0 +1,31 @@
+/*
+ * 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 { render } from '@openinula/next';
+function UserInput() {
+ let count = 0;
+ function incrementCount() {
+ count++;
+ }
+
+ return (
+ <>
+
{count}
+
+ >
+ );
+}
+
+render(UserInput, document.getElementById('app'));
diff --git a/demos/v2/vite.config.ts b/demos/v2/vite.config.ts
index b1f901d0..005fd20e 100644
--- a/demos/v2/vite.config.ts
+++ b/demos/v2/vite.config.ts
@@ -1,7 +1,10 @@
import { defineConfig } from 'vite';
-import inula from 'vite-plugin-inula-next';
+import inula from '@openinula/vite-plugin-inula-next';
export default defineConfig({
+ build: {
+ minify: false, // 设置为 false 可以关闭代码压缩
+ },
server: {
port: 4320,
},
diff --git a/packages/inula-next-shared/package.json b/packages/inula-next-shared/package.json
new file mode 100644
index 00000000..a81c26c9
--- /dev/null
+++ b/packages/inula-next-shared/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "@openinula/next-shared",
+ "version": "0.0.1",
+ "description": "Inula Next Shared",
+ "keywords": [
+ "Inula-Next"
+ ],
+ "license": "MIT",
+ "files": [
+ "src"
+ ],
+ "type": "module",
+ "main": "dist/index.js",
+ "scripts": {
+ "build": "tsup --sourcemap"
+ },
+ "devDependencies": {
+ "typescript": "^5.3.2",
+ "tsup": "^8.2.4"
+ },
+ "tsup": {
+ "entry": [
+ "src/index.ts"
+ ],
+ "format": [
+ "cjs",
+ "esm"
+ ],
+ "clean": true,
+ "dts": true,
+ "sourceMap": true
+ }
+}
diff --git a/packages/inula-next-shared/src/index.ts b/packages/inula-next-shared/src/index.ts
new file mode 100644
index 00000000..798bdbbc
--- /dev/null
+++ b/packages/inula-next-shared/src/index.ts
@@ -0,0 +1,13 @@
+export enum InulaNodeType {
+ Comp = 0,
+ For = 1,
+ Cond = 2,
+ Exp = 3,
+ Hook = 4,
+ Context = 5,
+ Children = 6,
+}
+
+export function getTypeName(type: InulaNodeType): string {
+ return InulaNodeType[type];
+}
diff --git a/packages/inula-next-shared/tsconfig.json b/packages/inula-next-shared/tsconfig.json
new file mode 100644
index 00000000..c9c265f5
--- /dev/null
+++ b/packages/inula-next-shared/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "moduleResolution": "Node",
+ "strict": true,
+ "esModuleInterop": true
+ },
+ "ts-node": {
+ "esm": true
+ }
+}
+
diff --git a/packages/inula-next-store/package.json b/packages/inula-next-store/package.json
index 880c3972..1736509a 100644
--- a/packages/inula-next-store/package.json
+++ b/packages/inula-next-store/package.json
@@ -1,13 +1,9 @@
{
"name": "@openinula/store",
- "version": "0.0.0",
- "description": "DLight shared store",
- "author": {
- "name": "IanDx",
- "email": "iandxssxx@gmail.com"
- },
+ "version": "0.0.1",
+ "description": "Inula shared store",
"keywords": [
- "dlight.js"
+ "Inula-Next"
],
"license": "MIT",
"files": [
diff --git a/packages/inula-next/README.md b/packages/inula-next/README.md
index 6f89397b..b23a459b 100644
--- a/packages/inula-next/README.md
+++ b/packages/inula-next/README.md
@@ -1,2 +1,2 @@
-# DLight Main Package
-See the website's documentations for usage.
+# Inula-Next Main Package
+See the website's documentations for usage.
diff --git a/packages/inula-next/package.json b/packages/inula-next/package.json
index 1698e98f..b0b17f3c 100644
--- a/packages/inula-next/package.json
+++ b/packages/inula-next/package.json
@@ -1,44 +1,41 @@
-{
- "name": "@openinula/next",
- "version": "0.0.1",
- "author": {
- "name": "IanDx",
- "email": "iandxssxx@gmail.com"
- },
- "keywords": [
- "inula"
- ],
- "license": "MIT",
- "files": [
- "dist",
- "README.md"
- ],
- "type": "module",
- "main": "dist/index.cjs",
- "module": "dist/index.js",
- "typings": "dist/index.d.ts",
- "scripts": {
- "build": "tsup --sourcemap && cp src/index.d.ts dist/ && cp -r src/types dist/",
- "test": "vitest --ui"
- },
- "dependencies": {
- "csstype": "^3.1.3",
- "@openinula/store": "workspace:*"
- },
- "devDependencies": {
- "tsup": "^6.5.0",
- "vite-plugin-inula-next": "workspace:*",
- "vitest": "^1.2.2"
- },
- "tsup": {
- "entry": [
- "src/index.js"
- ],
- "format": [
- "cjs",
- "esm"
- ],
- "clean": true,
- "minify": true
- }
-}
+{
+ "name": "@openinula/next",
+ "version": "0.0.2",
+ "keywords": [
+ "inula"
+ ],
+ "license": "MIT",
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "type": "module",
+ "main": "dist/index.cjs",
+ "module": "dist/index.js",
+ "typings": "dist/index.d.ts",
+ "scripts": {
+ "build": "tsup --sourcemap",
+ "test": "vitest --ui"
+ },
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "@openinula/next-shared": "workspace:*"
+ },
+ "devDependencies": {
+ "tsup": "^6.5.0",
+ "@openinula/vite-plugin-inula-next": "workspace:*",
+ "vitest": "2.0.5"
+ },
+ "tsup": {
+ "entry": [
+ "src/index.ts"
+ ],
+ "format": [
+ "cjs",
+ "esm"
+ ],
+ "clean": true,
+ "minify": false,
+ "noExternal": ["@openinula/next-shared"]
+ }
+}
diff --git a/packages/inula-next/src/ChildrenNode.ts b/packages/inula-next/src/ChildrenNode.ts
new file mode 100644
index 00000000..ad04d88d
--- /dev/null
+++ b/packages/inula-next/src/ChildrenNode.ts
@@ -0,0 +1,41 @@
+import { addWillUnmount } from './lifecycle';
+import { ChildrenNode, VNode, Updater } from './types';
+import { InulaNodeType } from '@openinula/next-shared';
+
+export function createChildrenNode(childrenFunc: (addUpdate: (updater: Updater) => void) => VNode[]): ChildrenNode {
+ return {
+ __type: InulaNodeType.Children,
+ childrenFunc,
+ updaters: new Set(),
+ };
+}
+
+/**
+ * @brief Build the prop view by calling the childrenFunc and add every single instance of the returned InulaNode to updaters
+ * @returns An array of InulaNode instances returned by childrenFunc
+ */
+export function buildChildren(childrenNode: ChildrenNode) {
+ let update;
+ const addUpdate = (updateFunc: Updater) => {
+ update = updateFunc;
+ childrenNode.updaters.add(updateFunc);
+ };
+ const newNodes = childrenNode.childrenFunc(addUpdate);
+ if (newNodes.length === 0) return [];
+ if (update) {
+ // Remove the updateNode from dlUpdateNodes when it unmounts
+ addWillUnmount(newNodes[0], childrenNode.updaters.delete.bind(childrenNode.updaters, update));
+ }
+
+ return newNodes;
+}
+
+/**
+ * @brief Update every node in dlUpdateNodes
+ * @param changed - A parameter indicating what changed to trigger the update
+ */
+export function updateChildrenNode(childrenNode: ChildrenNode, changed: number) {
+ childrenNode.updaters.forEach(update => {
+ update(changed);
+ });
+}
diff --git a/packages/inula-next/src/CompNode.js b/packages/inula-next/src/CompNode.js
deleted file mode 100644
index 1c32895c..00000000
--- a/packages/inula-next/src/CompNode.js
+++ /dev/null
@@ -1,373 +0,0 @@
-import { DLNode, DLNodeType } from './DLNode';
-import { forwardHTMLProp } from './HTMLNode';
-import { DLStore, cached } from './store';
-import { schedule } from './scheduler';
-
-/**
- * @class
- * @extends import('./DLNode').DLNode
- */
-export class CompNode extends DLNode {
- /**
- * @brief Constructor, Comp type
- * @internal
- * * key - private property key
- * * $$key - dependency number, e.g. 0b1, 0b10, 0b100
- * * $s$key - set of properties that depend on this property
- * * $p$key - exist if this property is a prop
- * * $e$key - exist if this property is an env
- * * $en$key - exist if this property is an env, and it's the innermost env that contains this env
- * * $w$key - exist if this property is a watcher
- * * $f$key - a function that returns the value of this property, called when the property's dependencies change
- * * _$children - children nodes of type PropView
- * * _$contentKey - the key key of the content prop
- * * _$forwardProps - exist if this node is forwarding props
- * * _$forwardPropsId - the keys of the props that this node is forwarding, collected in _$setForwardProp
- * * _$forwardPropsSet - contain all the nodes that are forwarding props to this node, collected with _$addForwardProps
- */
- constructor() {
- super(DLNodeType.Comp);
- }
-
- /**
- * @brief Init function, called explicitly in the subclass's constructor
- * @param props - Object containing properties
- * @param content - Content to be used
- * @param children - Child nodes
- * @param forwardPropsScope - Scope for forwarding properties
- */
- _$init(props, content, children, forwardPropsScope) {
- this._$notInitd = true;
-
- // ---- Forward props first to allow internal props to override forwarded props
- if (forwardPropsScope) forwardPropsScope._$addForwardProps(this);
- if (content) this._$setContent(() => content[0], content[1]);
- if (props)
- props.forEach(([key, value, deps]) => {
- if (key === 'props') return this._$setProps(() => value, deps);
- this._$setProp(key, () => value, deps);
- });
- if (children) this._$children = children;
-
- // ---- Add envs
- DLStore.global.DLEnvStore &&
- Object.entries(DLStore.global.DLEnvStore.envs).forEach(([key, [value, envNode]]) => {
- if (key === '_$catchable') {
- this._$catchable = value;
- return;
- }
- if (!(`$e$${key}` in this)) return;
- envNode.addNode(this);
- this._$initEnv(key, value, envNode);
- });
-
- const willCall = () => {
- this._$callUpdatesBeforeInit();
- this.didMount && DLNode.addDidMount(this, this.didMount.bind(this));
- this.willUnmount && DLNode.addWillUnmount(this, this.willUnmount.bind(this));
- DLNode.addDidUnmount(this, this._$setUnmounted.bind(this));
- this.didUnmount && DLNode.addDidUnmount(this, this.didUnmount.bind(this));
- this.willMount?.();
- this._$nodes = this.Body?.() ?? [];
- };
-
- if (this._$catchable) {
- this._$catchable(willCall)();
- if (this._$update) this._$update = this._$catchable(this._$update.bind(this));
- this._$updateDerived = this._$catchable(this._$updateDerived.bind(this));
- delete this._$catchable;
- } else {
- willCall();
- }
- }
-
- _$setUnmounted() {
- this._$unmounted = true;
- }
-
- /**
- * @brief Call updates manually before the node is mounted
- */
- _$callUpdatesBeforeInit() {
- const protoProps = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
- const ownProps = Object.getOwnPropertyNames(this);
- const allProps = [...protoProps, ...ownProps];
- allProps.forEach(key => {
- // ---- Run watcher
- if (key.startsWith('$w$')) return this[key.slice(3)]();
- // ---- Run model update
- if (key.startsWith('$md$')) {
- const realKey = key.slice(4);
- this[realKey] = this[realKey]();
- return;
- }
- // ---- Run derived value
- if (key.startsWith('$f$')) {
- const realKey = key.slice(3);
- this[realKey] = this[key];
- this._$updateDerived(realKey);
- }
- });
- delete this._$notInitd;
- }
-
- /**
- * @brief Set all the props to forward
- * @param key
- * @param value
- * @param deps
- */
- _$setPropToForward(key, value, deps) {
- this._$forwardPropsSet.forEach(node => {
- const isContent = key === '_$content';
- if (node._$dlNodeType === DLNodeType.Comp) {
- if (isContent) node._$setContent(() => value, deps);
- else node._$setProp(key, () => value, deps);
- return;
- }
- if (node instanceof HTMLElement) {
- if (isContent) key = 'textContent';
- forwardHTMLProp(node, key, () => value, deps);
- }
- });
- }
-
- /**
- * @brief Define forward props
- * @param key
- * @param value
- */
- _$setForwardProp(key, valueFunc, deps) {
- const notInitd = '_$notInitd' in this;
- if (!notInitd && this._$cache(key, deps)) return;
- const value = valueFunc();
- if (key === '_$content' && this._$contentKey) {
- this[this._$contentKey] = value;
- this._$updateDerived(this._$contentKey);
- }
- this[key] = value;
- this._$updateDerived(key);
- if (notInitd) this._$forwardPropsId.push(key);
- else this._$setPropToForward(key, value, deps);
- }
-
- /**
- * @brief Add a node to the set of nodes that are forwarding props to this node and init these props
- * @param node
- */
- _$addForwardProps(node) {
- this._$forwardPropsSet.add(node);
- this._$forwardPropsId.forEach(key => {
- this._$setPropToForward(key, this[key], []);
- });
- DLNode.addWillUnmount(node, this._$forwardPropsSet.delete.bind(this._$forwardPropsSet, node));
- }
-
- /**
- * @brief Cache the deps and return true if the deps are the same as the previous deps
- * @param key
- * @param deps
- * @returns
- */
- _$cache(key, deps) {
- if (!deps || !deps.length) return false;
- const cacheKey = `$cc$${key}`;
- if (cached(deps, this[cacheKey])) return true;
- this[cacheKey] = deps;
- return false;
- }
-
- /**
- * @brief Set the content prop, the key is stored in _$contentKey
- * @param value
- */
- _$setContent(valueFunc, deps) {
- if ('_$forwardProps' in this) return this._$setForwardProp('_$content', valueFunc, deps);
- const contentKey = this._$contentKey;
- if (!contentKey) return;
- if (this._$cache(contentKey, deps)) return;
- this[contentKey] = valueFunc();
- this._$updateDerived(contentKey);
- }
-
- /**
- * @brief Set a prop directly, if this is a forwarded prop, go and init forwarded props
- * @param key
- * @param value
- * @param deps
- */
- _$setProp(key, valueFunc, deps) {
- if ('_$forwardProps' in this) return this._$setForwardProp(key, valueFunc, deps);
- if (!(`$p$${key}` in this)) {
- console.warn(`[${key}] is not a prop in ${this.constructor.name}`);
- return;
- }
- if (this._$cache(key, deps)) return;
- this[key] = valueFunc();
- this._$updateDerived(key);
- }
-
- _$setProps(valueFunc, deps) {
- if (this._$cache('props', deps)) return;
- const props = valueFunc();
- if (!props) return;
- Object.entries(props).forEach(([key, value]) => {
- this._$setProp(key, () => value, []);
- });
- }
-
- /**
- * @brief Init an env, put the corresponding innermost envNode in $en$key
- * @param key
- * @param value
- * @param envNode
- */
- _$initEnv(key, value, envNode) {
- this[key] = value;
- this[`$en$${key}`] = envNode;
- }
-
- // ---- Update functions
- /**
- * @brief Update an env, called in EnvNode._$update
- * @param key
- * @param value
- * @param envNode
- */
- _$updateEnv(key, value, envNode) {
- if (!(`$e$${key}` in this)) return;
- if (envNode !== this[`$en$${key}`]) return;
- this[key] = value;
- this._$updateDerived(key);
- }
-
- /**
- * @brief Update a prop
- */
- _$ud(exp, key) {
- this._$updateDerived(key);
- return exp;
- }
-
- /**
- * @brief Update properties that depend on this property
- * @param key
- */
- _$updateDerived(key) {
- if ('_$notInitd' in this) return;
-
- this[`$s$${key}`]?.forEach(k => {
- if (`$w$${k}` in this) {
- // ---- Watcher
- this[k](key);
- } else if (`$md$${k}` in this) {
- this[k]._$update();
- } else {
- // ---- Regular derived value
- this[k] = this[`$f$${k}`];
- }
- });
-
- // ---- "trigger-view"
- this._$updateView(key);
- }
-
- _$updateView(key) {
- if (this._$modelCallee) return this._$updateModelCallee();
- if (!('_$update' in this)) return;
- const depNum = this[`$$${key}`];
- if (!depNum) return;
- // ---- Collect all depNums that need to be updated
- if ('_$depNumsToUpdate' in this) {
- this._$depNumsToUpdate.push(depNum);
- } else {
- this._$depNumsToUpdate = [depNum];
- // ---- Update in the next microtask
- schedule(() => {
- // ---- Abort if unmounted
- if (this._$unmounted) return;
- const depNums = this._$depNumsToUpdate;
- if (depNums.length > 0) {
- const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
- this._$update(depNum);
- }
- delete this._$depNumsToUpdate;
- });
- }
- }
-
- _$updateModelCallee() {
- if ('_$depNumsToUpdate' in this) return;
- this._$depNumsToUpdate = true;
- // ---- Update in the next microtask
- Promise.resolve().then(() => {
- // ---- Abort if unmounted
- if (this._$unmounted) return;
- this._$modelCallee._$updateDerived(this._$modelKey);
- delete this._$depNumsToUpdate;
- });
- }
-
- /**
- * @brief Update all props and content of the model
- */
- static _$updateModel(model, propsFunc, contentFunc) {
- // ---- Suppress update because top level update will be performed
- // directly by the state variable in the model callee, which will
- // trigger the update of the model
- const props = propsFunc() ?? {};
- const collectedProps = props.s ?? [];
- props.m?.forEach(([props, deps]) => {
- Object.entries(props).forEach(([key, value]) => {
- collectedProps.push([key, value, deps]);
- });
- });
- collectedProps.forEach(([key, value, deps]) => {
- model._$setProp(key, () => value, deps);
- });
- const content = contentFunc();
- if (content) model._$setContent(() => content[0], content[1]);
- }
-
- static _$releaseModel() {
- delete this._$modelCallee;
- }
-
- /**
- * @brief Inject Dlight model in to a property
- * @param ModelCls
- * @param props { m: [props, deps], s: [key, value, deps] }
- * @param content
- * @param key
- * @returns
- */
- _$injectModel(ModelCls, propsFunc, contentFunc, key) {
- const props = propsFunc() ?? {};
- const collectedProps = props.s ?? [];
- props.m?.forEach(([props, deps]) => {
- Object.entries(props).forEach(([key, value]) => {
- collectedProps.push([key, value, deps]);
- });
- });
- const model = new ModelCls();
- model._$init(collectedProps, contentFunc(), null, null);
- model._$modelCallee = this;
- model._$modelKey = key;
- model._$update = CompNode._$updateModel.bind(null, model, propsFunc, contentFunc);
-
- return model;
- }
-}
-
-// ---- @View -> class Comp extends View
-export const View = CompNode;
-export const Model = CompNode;
-
-/**
- * @brief Run all update functions given the key
- * @param dlNode
- * @param key
- */
-export function update(dlNode, key) {
- dlNode._$updateDerived(key);
-}
diff --git a/packages/inula-next/src/CompNode.ts b/packages/inula-next/src/CompNode.ts
new file mode 100644
index 00000000..c2636ee9
--- /dev/null
+++ b/packages/inula-next/src/CompNode.ts
@@ -0,0 +1,138 @@
+import { addDidMount, addDidUnmount, addWillUnmount } from './lifecycle';
+import { equal } from './equal';
+import { schedule } from './scheduler';
+import { inMount } from './index';
+import { CompNode, ComposableNode } from './types';
+import { InulaNodeType } from '@openinula/next-shared';
+
+export function createCompNode(): CompNode {
+ return {
+ updateProp: builtinUpdateFunc,
+ updateState: builtinUpdateFunc,
+ __type: InulaNodeType.Comp,
+ props: {},
+ _$nodes: [],
+ };
+}
+
+export function builtinUpdateFunc() {
+ throw new Error('Component node not initiated.');
+}
+
+export function constructComp(
+ comp: CompNode,
+ {
+ updateState,
+ updateProp,
+ updateContext,
+ getUpdateViews,
+ didUnmount,
+ willUnmount,
+ didMount,
+ }: Pick<
+ CompNode,
+ 'updateState' | 'updateProp' | 'updateContext' | 'getUpdateViews' | 'didUnmount' | 'willUnmount' | 'didMount'
+ >
+): CompNode {
+ comp.updateState = updateState;
+ comp.updateProp = updateProp;
+ comp.updateContext = updateContext;
+ comp.getUpdateViews = getUpdateViews;
+ comp.didUnmount = didUnmount;
+ comp.willUnmount = willUnmount;
+ comp.didMount = didMount;
+
+ return comp;
+}
+
+export function initCompNode(node: CompNode): CompNode {
+ node.mounting = true;
+ const willCall = () => {
+ callUpdatesBeforeInit(node);
+ if (node.didMount) addDidMount(node, node.didMount);
+ if (node.willUnmount) addWillUnmount(node, node.willUnmount);
+ addDidUnmount(node, setUnmounted.bind(null, node));
+ if (node.didUnmount) addDidUnmount(node, node.didUnmount);
+ if (node.getUpdateViews) {
+ const result = node.getUpdateViews();
+ if (Array.isArray(result)) {
+ const [baseNode, updateView] = result;
+ node.updateView = updateView;
+ node._$nodes = baseNode;
+ } else {
+ node.updateView = result;
+ }
+ }
+ };
+
+ willCall();
+
+ return node;
+}
+
+function setUnmounted(node: CompNode) {
+ node._$unmounted = true;
+}
+
+function callUpdatesBeforeInit(node: CompNode) {
+ node.updateState(-1);
+ delete node.mounting;
+}
+
+function cacheCheck(node: CompNode, key: string, deps: any[]): boolean {
+ if (!deps || !deps.length) return false;
+ if (!node.cache) {
+ node.cache = {};
+ }
+ if (equal(deps, node.cache[key])) return true;
+ node.props[key] = deps;
+ return false;
+}
+
+export function setProp(node: CompNode, key: string, valueFunc: () => any, deps: any[]) {
+ if (cacheCheck(node, key, deps)) return;
+ node.props[key] = valueFunc();
+ node.updateProp(key, node.props[key]);
+}
+
+export function setProps(node: CompNode, valueFunc: () => Record, deps: any[]) {
+ if (cacheCheck(node, 'props', deps)) return;
+ const props = valueFunc();
+ if (!props) return;
+ Object.entries(props).forEach(([key, value]) => {
+ setProp(node, key, () => value, []);
+ });
+}
+
+export function updateContext(node: CompNode, key: string, value: any, context: any) {
+ if (!node.updateContext) return;
+ node.updateContext(context, key, value);
+}
+
+export function updateCompNode(node: ComposableNode, newValue: any, bit?: number) {
+ if ('mounting' in node) return;
+
+ node.updateState(bit || 0);
+
+ if (!inMount()) {
+ updateView(node, bit || 0);
+ }
+}
+
+function updateView(node: ComposableNode, bit: number) {
+ if (!bit) return;
+ if ('_$depNumsToUpdate' in node) {
+ node._$depNumsToUpdate?.push(bit);
+ } else {
+ node._$depNumsToUpdate = [bit];
+ schedule(() => {
+ if (node._$unmounted) return;
+ const depNums = node._$depNumsToUpdate || [];
+ if (depNums.length > 0) {
+ const depNum = depNums.reduce((acc, cur) => acc | cur, 0);
+ node.updateView?.(depNum);
+ }
+ delete node._$depNumsToUpdate;
+ });
+ }
+}
diff --git a/packages/inula-next/src/ContextNode.ts b/packages/inula-next/src/ContextNode.ts
new file mode 100644
index 00000000..ef10dedf
--- /dev/null
+++ b/packages/inula-next/src/ContextNode.ts
@@ -0,0 +1,122 @@
+import { InulaNodeType } from '@openinula/next-shared';
+import { addWillUnmount } from './lifecycle';
+import { equal } from './equal';
+import { VNode, ContextNode, Context, CompNode, HookNode } from './types';
+import { currentComp } from '.';
+
+let contextNodeMap: Map>;
+
+export function getContextNodeMap() {
+ return contextNodeMap;
+}
+
+export function createContextNode>(
+ ctx: Context,
+ value: V,
+ depMap: Record>
+) {
+ if (!contextNodeMap) contextNodeMap = new Map();
+
+ const ContextNode: ContextNode = {
+ value: value,
+ depMap: depMap,
+ context: ctx,
+ __type: InulaNodeType.Context,
+ consumers: new Set(),
+ _$nodes: [],
+ };
+
+ replaceContextValue(ContextNode);
+
+ return ContextNode;
+}
+
+/**
+ * @brief Update a specific key of context, and update all the comp nodes that depend on this context
+ * @param contextNode
+ * @param name - The name of the environment variable to update
+ * @param valueFunc
+ * @param deps
+ */
+export function updateContextNode>(
+ contextNode: ContextNode,
+ name: keyof V,
+ valueFunc: () => V[keyof V],
+ deps: Array
+) {
+ if (cached(contextNode, deps, name)) return;
+ const value = valueFunc();
+ contextNode.value[name] = value;
+ contextNode.consumers.forEach(node => {
+ // should have updateContext, otherwise the bug of compiler
+ node.updateContext!(contextNode.context, name as string, value);
+ });
+}
+
+function cached>(contextNode: ContextNode, deps: Array, name: keyof V) {
+ if (!deps || !deps.length) return false;
+ if (equal(deps, contextNode.depMap[name])) return true;
+ contextNode.depMap[name] = deps;
+ return false;
+}
+
+function replaceContextValue>(contextNode: ContextNode) {
+ contextNode.prevValue = contextNode.context.value;
+ contextNode.prevContextNode = contextNodeMap!.get(contextNode.context.id);
+ contextNode.context.value = contextNode.value;
+
+ contextNodeMap!.set(contextNode.context.id, contextNode);
+}
+
+/**
+ * @brief Set this._$nodes, and exit the current context
+ * @param contextNode
+ * @param nodes - The nodes to set
+ */
+export function initContextChildren>(contextNode: ContextNode, nodes: VNode[]) {
+ contextNode._$nodes = nodes;
+ contextNode.context.value = contextNode.prevValue || null;
+ if (contextNode.prevContextNode) {
+ contextNodeMap!.set(contextNode.context.id, contextNode.prevContextNode);
+ } else {
+ contextNodeMap!.delete(contextNode.context.id);
+ }
+ contextNode.prevValue = null;
+ contextNode.prevContextNode = null;
+}
+
+export function replaceContext(contextNodeMap: Map>) {
+ for (const [ctxId, contextNode] of contextNodeMap.entries()) {
+ replaceContextValue(contextNode);
+ }
+}
+
+/**
+ * @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
+ */
+export function addConsumer(contextNode: ContextNode, node: CompNode | HookNode) {
+ contextNode.consumers.add(node);
+ addWillUnmount(node, contextNode.consumers.delete.bind(contextNode.consumers, node));
+}
+
+export function createContext | null>(defaultVal: T): Context {
+ return {
+ id: Symbol('inula-ctx'),
+ value: defaultVal,
+ };
+}
+
+export function useContext | null>(ctx: Context, key?: keyof T): T | T[keyof T] {
+ if (contextNodeMap) {
+ const contextNode = contextNodeMap.get(ctx.id);
+ if (contextNode) {
+ addConsumer(contextNode, currentComp!);
+ }
+ }
+
+ if (key && ctx.value) {
+ return ctx.value[key];
+ }
+
+ return ctx.value as T;
+}
diff --git a/packages/inula-next/src/DLNode.js b/packages/inula-next/src/DLNode.js
deleted file mode 100644
index d7b7fb8e..00000000
--- a/packages/inula-next/src/DLNode.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import { DLStore } from './store';
-
-export const DLNodeType = {
- Comp: 0,
- For: 1,
- Cond: 2,
- Env: 3,
- Exp: 4,
- Snippet: 5,
- Try: 6,
-};
-
-export class DLNode {
- /**
- * @brief Node type: HTML, Text, Custom, For, If, Env, Expression
- */
- _$dlNodeType;
-
- /**
- * @brief Constructor
- * @param nodeType
- * @return {void}
- */
- constructor(nodeType) {
- this._$dlNodeType = nodeType;
- }
-
- /**
- * @brief Node element
- * Either one real element for HTMLNode and TextNode
- * Or an array of DLNode for CustomNode, ForNode, IfNode, EnvNode, ExpNode
- */
- get _$el() {
- return DLNode.toEls(this._$nodes);
- }
-
- /**
- * @brief Loop all child DLNodes to get all the child elements
- * @param nodes
- * @returns HTMLElement[]
- */
- static toEls(nodes) {
- const els = [];
- this.loopShallowEls(nodes, el => {
- els.push(el);
- });
- return els;
- }
-
- // ---- Loop nodes ----
- /**
- * @brief Loop all elements shallowly,
- * i.e., don't loop the child nodes of dom elements and only call runFunc on dom elements
- * @param nodes
- * @param runFunc
- */
- static loopShallowEls(nodes, runFunc) {
- const stack = [...nodes].reverse();
- while (stack.length > 0) {
- const node = stack.pop();
- if (!('_$dlNodeType' in node)) runFunc(node);
- else node._$nodes && stack.push(...[...node._$nodes].reverse());
- }
- }
-
- /**
- * @brief Add parentEl to all nodes until the first element
- * @param nodes
- * @param parentEl
- */
- static addParentEl(nodes, parentEl) {
- nodes.forEach(node => {
- if ('_$dlNodeType' in node) {
- node._$parentEl = parentEl;
- node._$nodes && DLNode.addParentEl(node._$nodes, parentEl);
- }
- });
- }
-
- // ---- Flow index and add child elements ----
- /**
- * @brief Get the total count of dom elements before the stop node
- * @param nodes
- * @param stopNode
- * @returns total count of dom elements
- */
- static getFlowIndexFromNodes(nodes, stopNode) {
- let index = 0;
- const stack = [...nodes].reverse();
- while (stack.length > 0) {
- const node = stack.pop();
- if (node === stopNode) break;
- if ('_$dlNodeType' in node) {
- node._$nodes && stack.push(...[...node._$nodes].reverse());
- } else {
- index++;
- }
- }
- return index;
- }
-
- /**
- * @brief Given an array of nodes, append them to the parentEl
- * 1. If nextSibling is provided, insert the nodes before the nextSibling
- * 2. If nextSibling is not provided, append the nodes to the parentEl
- * @param nodes
- * @param parentEl
- * @param nextSibling
- * @returns Added element count
- */
- static appendNodesWithSibling(nodes, parentEl, nextSibling) {
- if (nextSibling) return this.insertNodesBefore(nodes, parentEl, nextSibling);
- return this.appendNodes(nodes, parentEl);
- }
-
- /**
- * @brief Given an array of nodes, append them to the parentEl using the index
- * 1. If the index is the same as the length of the parentEl.childNodes, append the nodes to the parentEl
- * 2. If the index is not the same as the length of the parentEl.childNodes, insert the nodes before the node at the index
- * @param nodes
- * @param parentEl
- * @param index
- * @param length
- * @returns Added element count
- */
- static appendNodesWithIndex(nodes, parentEl, index, length) {
- length = length ?? parentEl.childNodes.length;
- if (length !== index) return this.insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
- return this.appendNodes(nodes, parentEl);
- }
-
- /**
- * @brief Insert nodes before the nextSibling
- * @param nodes
- * @param parentEl
- * @param nextSibling
- * @returns Added element count
- */
- static insertNodesBefore(nodes, parentEl, nextSibling) {
- let count = 0;
- this.loopShallowEls(nodes, el => {
- parentEl.insertBefore(el, nextSibling);
- count++;
- });
- return count;
- }
-
- /**
- * @brief Append nodes to the parentEl
- * @param nodes
- * @param parentEl
- * @returns Added element count
- */
- static appendNodes(nodes, parentEl) {
- let count = 0;
- this.loopShallowEls(nodes, el => {
- parentEl.appendChild(el);
- count++;
- });
- return count;
- }
-
- // ---- Lifecycle ----
- /**
- * @brief Add willUnmount function to node
- * @param node
- * @param func
- */
- static addWillUnmount(node, func) {
- const willUnmountStore = DLStore.global.WillUnmountStore;
- const currentStore = willUnmountStore[willUnmountStore.length - 1];
- // ---- If the current store is empty, it means this node is not mutable
- if (!currentStore) return;
- currentStore.push(func.bind(null, node));
- }
-
- /**
- * @brief Add didUnmount function to node
- * @param node
- * @param func
- */
- static addDidUnmount(node, func) {
- const didUnmountStore = DLStore.global.DidUnmountStore;
- const currentStore = didUnmountStore[didUnmountStore.length - 1];
- // ---- If the current store is empty, it means this node is not mutable
- if (!currentStore) return;
- currentStore.push(func.bind(null, node));
- }
-
- /**
- * @brief Add didUnmount function to global store
- * @param func
- */
- static addDidMount(node, func) {
- if (!DLStore.global.DidMountStore) DLStore.global.DidMountStore = [];
- DLStore.global.DidMountStore.push(func.bind(null, node));
- }
-
- /**
- * @brief Run all didMount functions and reset the global store
- */
- static runDidMount() {
- const didMountStore = DLStore.global.DidMountStore;
- if (!didMountStore || didMountStore.length === 0) return;
- for (let i = didMountStore.length - 1; i >= 0; i--) {
- didMountStore[i]();
- }
- DLStore.global.DidMountStore = [];
- }
-}
diff --git a/packages/inula-next/src/EnvNode.js b/packages/inula-next/src/EnvNode.js
deleted file mode 100644
index 22b1b4c0..00000000
--- a/packages/inula-next/src/EnvNode.js
+++ /dev/null
@@ -1,103 +0,0 @@
-import { DLNode, DLNodeType } from './DLNode';
-import { DLStore, cached } from './store';
-
-export class EnvStoreClass {
- constructor() {
- this.envs = {};
- this.currentEnvNodes = [];
- }
-
- /**
- * @brief Add a node to the current env and merge envs
- * @param node - The node to add
- */
- addEnvNode(node) {
- this.currentEnvNodes.push(node);
- this.mergeEnvs();
- }
-
- /**
- * @brief Replace the current env with the given nodes and merge envs
- * @param nodes - The nodes to replace the current environment with
- */
- replaceEnvNodes(nodes) {
- this.currentEnvNodes = nodes;
- this.mergeEnvs();
- }
-
- /**
- * @brief Remove the last node from the current env and merge envs
- */
- removeEnvNode() {
- this.currentEnvNodes.pop();
- this.mergeEnvs();
- }
-
- /**
- * @brief Merge all the envs in currentEnvNodes, inner envs override outer envs
- */
- mergeEnvs() {
- this.envs = {};
- this.currentEnvNodes.forEach(envNode => {
- Object.entries(envNode.envs).forEach(([key, value]) => {
- this.envs[key] = [value, envNode];
- });
- });
- }
-}
-
-export class EnvNode extends DLNode {
- constructor(envs, depsArr) {
- super(DLNodeType.Env);
- // Declare a global variable to store the environment variables
- if (!('DLEnvStore' in DLStore.global)) DLStore.global.DLEnvStore = new EnvStoreClass();
-
- this.envs = envs;
- this.depsArr = depsArr;
- this.updateNodes = new Set();
-
- DLStore.global.DLEnvStore.addEnvNode(this);
- }
-
- cached(deps, name) {
- if (!deps || !deps.length) return false;
- if (cached(deps, this.depsArr[name])) return true;
- this.depsArr[name] = deps;
- return false;
- }
-
- /**
- * @brief Update a specific env, and update all the comp nodes that depend on this env
- * @param name - The name of the environment variable to update
- * @param value - The new value of the environment variable
- */
- updateEnv(name, valueFunc, deps) {
- if (this.cached(deps, name)) return;
- const value = valueFunc();
- this.envs[name] = value;
- if (DLStore.global.DLEnvStore.currentEnvNodes.includes(this)) {
- DLStore.global.DLEnvStore.mergeEnvs();
- }
- this.updateNodes.forEach(node => {
- node._$updateEnv(name, value, this);
- });
- }
-
- /**
- * @brief Add a node to this.updateNodes, delete the node from this.updateNodes when it unmounts
- * @param node - The node to add
- */
- addNode(node) {
- this.updateNodes.add(node);
- DLNode.addWillUnmount(node, this.updateNodes.delete.bind(this.updateNodes, node));
- }
-
- /**
- * @brief Set this._$nodes, and exit the current env
- * @param nodes - The nodes to set
- */
- initNodes(nodes) {
- this._$nodes = nodes;
- DLStore.global.DLEnvStore.removeEnvNode();
- }
-}
diff --git a/packages/inula-next/src/HTMLNode.js b/packages/inula-next/src/HTMLNode.js
deleted file mode 100644
index f8f8ab4a..00000000
--- a/packages/inula-next/src/HTMLNode.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { DLNode } from './DLNode';
-import { DLStore, cached } from './store';
-
-function cache(el, key, deps) {
- if (deps.length === 0) return false;
- const cacheKey = `$${key}`;
- if (cached(deps, el[cacheKey])) return true;
- el[cacheKey] = deps;
- return false;
-}
-
-/**
- * @brief Plainly set style
- * @param el
- * @param value
- */
-export function setStyle(el, value) {
- Object.entries(value).forEach(([key, value]) => {
- if (key.startsWith('--')) {
- el.style.setProperty(key, value);
- } else {
- el.style[key] = value;
- }
- });
-}
-
-/**
- * @brief Plainly set dataset
- * @param el
- * @param value
- */
-export function setDataset(el, value) {
- Object.assign(el.dataset, value);
-}
-
-/**
- * @brief Set HTML property with checking value equality first
- * @param el
- * @param key
- * @param value
- */
-export function setHTMLProp(el, key, valueFunc, deps) {
- // ---- Comparing deps, same value won't trigger
- // will lead to a bug if the value is set outside of the DLNode
- // e.g. setHTMLProp(el, "textContent", "value", [])
- // => el.textContent = "other"
- // => setHTMLProp(el, "textContent", "value", [])
- // The value will be set to "other" instead of "value"
- if (cache(el, key, deps)) return;
- el[key] = valueFunc();
-}
-
-/**
- * @brief Plainly set HTML properties
- * @param el
- * @param value
- */
-export function setHTMLProps(el, value) {
- Object.entries(value).forEach(([key, v]) => {
- if (key === 'style') return setStyle(el, v);
- if (key === 'dataset') return setDataset(el, v);
- setHTMLProp(el, key, () => v, []);
- });
-}
-
-/**
- * @brief Set HTML attribute with checking value equality first
- * @param el
- * @param key
- * @param value
- */
-export function setHTMLAttr(el, key, valueFunc, deps) {
- if (cache(el, key, deps)) return;
- el.setAttribute(key, valueFunc());
-}
-
-/**
- * @brief Plainly set HTML attributes
- * @param el
- * @param value
- */
-export function setHTMLAttrs(el, value) {
- Object.entries(value).forEach(([key, v]) => {
- setHTMLAttr(el, key, () => v, []);
- });
-}
-
-/**
- * @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
- * @param el
- * @param key
- * @param value
- */
-export function setEvent(el, key, value) {
- const prevEvent = el[`$on${key}`];
- if (prevEvent) el.removeEventListener(key, prevEvent);
- el.addEventListener(key, value);
- el[`$on${key}`] = value;
-}
-
-function eventHandler(e) {
- const key = `$$${e.type}`;
- for (const node of e.composedPath()) {
- if (node[key]) node[key](e);
- if (e.cancelBubble) return;
- }
-}
-
-export function delegateEvent(el, key, value) {
- if (el[`$$${key}`] === value) return;
- el[`$$${key}`] = value;
- if (!DLStore.delegatedEvents.has(key)) {
- DLStore.delegatedEvents.add(key);
- DLStore.document.addEventListener(key, eventHandler);
- }
-}
-/**
- * @brief Shortcut for document.createElement
- * @param tag
- * @returns HTMLElement
- */
-export function createElement(tag) {
- return DLStore.document.createElement(tag);
-}
-
-/**
- * @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
- * @param el
- * @param node
- * @param position
- */
-export function insertNode(el, node, position) {
- // ---- Set _$nodes
- if (!el._$nodes) el._$nodes = Array.from(el.childNodes);
- el._$nodes.splice(position, 0, node);
-
- // ---- Insert nodes' elements
- const flowIdx = DLNode.getFlowIndexFromNodes(el._$nodes, node);
- DLNode.appendNodesWithIndex([node], el, flowIdx);
- // ---- Set parentEl
- DLNode.addParentEl([node], el);
-}
-
-/**
- * @brief An inclusive assign prop function that accepts any type of prop
- * @param el
- * @param key
- * @param value
- */
-export function forwardHTMLProp(el, key, valueFunc, deps) {
- if (key === 'style') return setStyle(el, valueFunc());
- if (key === 'dataset') return setDataset(el, valueFunc());
- if (key === 'element') return;
- if (key === 'prop') return setHTMLProps(el, valueFunc());
- if (key === 'attr') return setHTMLAttrs(el, valueFunc());
- if (key === 'innerHTML') return setHTMLProp(el, 'innerHTML', valueFunc, deps);
- if (key === 'textContent') return setHTMLProp(el, 'textContent', valueFunc, deps);
- if (key === 'forwardProp') return;
- if (key.startsWith('on')) {
- return setEvent(el, key.slice(2).toLowerCase(), valueFunc());
- }
- setHTMLAttr(el, key, valueFunc, deps);
-}
diff --git a/packages/inula-next/src/HookNode.ts b/packages/inula-next/src/HookNode.ts
new file mode 100644
index 00000000..c6206a29
--- /dev/null
+++ b/packages/inula-next/src/HookNode.ts
@@ -0,0 +1,70 @@
+/*
+ * 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 { builtinUpdateFunc, inMount, updateCompNode } from './index.js';
+import { CompNode, HookNode } from './types';
+import { InulaNodeType } from '@openinula/next-shared';
+
+export function createHookNode(parent: HookNode | CompNode, bitmap: number): HookNode {
+ return {
+ updateProp: builtinUpdateFunc,
+ updateState: builtinUpdateFunc,
+ __type: InulaNodeType.Hook,
+ props: {},
+ _$nodes: [],
+ bitmap,
+ parent,
+ };
+}
+
+export function emitUpdate(node: HookNode) {
+ // the new value is not used in the `updateCompNode`, just pass a null
+ updateCompNode(node.parent, null, node.bitmap);
+}
+
+export function constructHook(
+ node: HookNode,
+ {
+ value,
+ updateState,
+ updateProp,
+ updateContext,
+ getUpdateViews,
+ didUnmount,
+ willUnmount,
+ didMount,
+ }: Pick<
+ HookNode,
+ | 'value'
+ | 'updateState'
+ | 'updateProp'
+ | 'updateContext'
+ | 'getUpdateViews'
+ | 'didUnmount'
+ | 'willUnmount'
+ | 'didMount'
+ >
+): HookNode {
+ node.value = value;
+ node.updateState = updateState;
+ node.updateProp = updateProp;
+ node.updateContext = updateContext;
+ node.getUpdateViews = getUpdateViews;
+ node.didUnmount = didUnmount;
+ node.willUnmount = willUnmount;
+ node.didMount = didMount;
+
+ return node;
+}
diff --git a/packages/inula-next/src/InulaNode.ts b/packages/inula-next/src/InulaNode.ts
new file mode 100644
index 00000000..01b026e3
--- /dev/null
+++ b/packages/inula-next/src/InulaNode.ts
@@ -0,0 +1,83 @@
+import { InulaHTMLNode, VNode, TextNode, InulaNode } from './types';
+
+export const getEl = (node: VNode): Array => {
+ return toEls(node._$nodes || []);
+};
+
+export const toEls = (nodes: InulaNode[]): Array => {
+ const els: Array = [];
+ loopShallowEls(nodes, el => {
+ els.push(el);
+ });
+ return els;
+};
+
+export const loopShallowEls = (nodes: InulaNode[], runFunc: (el: InulaHTMLNode | TextNode) => void): void => {
+ const stack: Array = [...nodes].reverse();
+ while (stack.length > 0) {
+ const node = stack.pop()!;
+ if (node instanceof HTMLElement || node instanceof Text) {
+ runFunc(node);
+ } else if (node._$nodes) {
+ stack.push(...[...node._$nodes].reverse());
+ }
+ }
+};
+
+export const addParentEl = (nodes: Array, parentEl: HTMLElement): void => {
+ nodes.forEach(node => {
+ if ('__type' in node) {
+ node._$parentEl = parentEl as InulaHTMLNode;
+ node._$nodes && addParentEl(node._$nodes, parentEl);
+ }
+ });
+};
+
+export const getFlowIndexFromNodes = (nodes: InulaNode[], stopNode?: InulaNode): number => {
+ let index = 0;
+ const stack: InulaNode[] = [...nodes].reverse();
+ while (stack.length > 0) {
+ const node = stack.pop()!;
+ if (node === stopNode) break;
+ if ('__type' in node) {
+ node._$nodes && stack.push(...[...node._$nodes].reverse());
+ } else {
+ index++;
+ }
+ }
+ return index;
+};
+
+export const appendNodesWithSibling = (nodes: Array, parentEl: HTMLElement, nextSibling?: Node): number => {
+ if (nextSibling) return insertNodesBefore(nodes, parentEl, nextSibling);
+ return appendNodes(nodes, parentEl);
+};
+
+export const appendNodesWithIndex = (
+ nodes: InulaNode[],
+ parentEl: HTMLElement,
+ index: number,
+ length?: number
+): number => {
+ length = length ?? parentEl.childNodes.length;
+ if (length !== index) return insertNodesBefore(nodes, parentEl, parentEl.childNodes[index]);
+ return appendNodes(nodes, parentEl);
+};
+
+export const insertNodesBefore = (nodes: InulaNode[], parentEl: HTMLElement, nextSibling: Node): number => {
+ let count = 0;
+ loopShallowEls(nodes, el => {
+ parentEl.insertBefore(el, nextSibling);
+ count++;
+ });
+ return count;
+};
+
+const appendNodes = (nodes: InulaNode[], parentEl: HTMLElement): number => {
+ let count = 0;
+ loopShallowEls(nodes, el => {
+ parentEl.appendChild(el);
+ count++;
+ });
+ return count;
+};
diff --git a/packages/inula-next/src/MutableNode/CondNode.js b/packages/inula-next/src/MutableNode/CondNode.js
deleted file mode 100644
index dbf75f48..00000000
--- a/packages/inula-next/src/MutableNode/CondNode.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { DLNodeType } from '../DLNode';
-import { FlatNode } from './FlatNode';
-
-export class CondNode extends FlatNode {
- /**
- * @brief Constructor, If type, accept a function that returns a list of nodes
- * @param caseFunc
- */
- constructor(depNum, condFunc) {
- super(DLNodeType.Cond);
- this.depNum = depNum;
- this.cond = -1;
- this.condFunc = condFunc;
- this.initUnmountStore();
- this._$nodes = this.condFunc(this);
- this.setUnmountFuncs();
-
- // ---- Add to the global UnmountStore
- CondNode.addWillUnmount(this, this.runWillUnmount.bind(this));
- CondNode.addDidUnmount(this, this.runDidUnmount.bind(this));
- }
-
- /**
- * @brief Update the nodes in the environment
- */
- updateCond(key) {
- // ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
- // The reason is that if it didn't change, we don't need to unmount or remove the nodes
- const prevFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
- const newNodes = this.geneNewNodesInEnv(() => this.condFunc(this));
-
- // ---- If the new nodes are the same as the old nodes, we only need to update children
- if (this.didntChange) {
- [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
- this.didntChange = false;
- this.updateFunc?.(this.depNum, key);
- return;
- }
- // ---- Remove old nodes
- const newFuncs = [this.willUnmountFuncs, this.didUnmountFuncs];
- [this.willUnmountFuncs, this.didUnmountFuncs] = prevFuncs;
- this._$nodes && this._$nodes.length > 0 && this.removeNodes(this._$nodes);
- [this.willUnmountFuncs, this.didUnmountFuncs] = newFuncs;
-
- if (newNodes.length === 0) {
- // ---- No branch has been taken
- this._$nodes = [];
- return;
- }
- // ---- Add new nodes
- const parentEl = this._$parentEl;
- // ---- Faster append with nextSibling rather than flowIndex
- const flowIndex = CondNode.getFlowIndexFromNodes(parentEl._$nodes, this);
-
- const nextSibling = parentEl.childNodes[flowIndex];
- CondNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
- CondNode.runDidMount();
- this._$nodes = newNodes;
- }
-
- /**
- * @brief The update function of IfNode's childNodes is stored in the first child node
- * @param changed
- */
- update(changed) {
- if (!(~this.depNum & changed)) return;
- this.updateFunc?.(changed);
- }
-}
diff --git a/packages/inula-next/src/MutableNode/CondNode.ts b/packages/inula-next/src/MutableNode/CondNode.ts
new file mode 100644
index 00000000..932bdf67
--- /dev/null
+++ b/packages/inula-next/src/MutableNode/CondNode.ts
@@ -0,0 +1,91 @@
+import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode.js';
+import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle.js';
+import {
+ geneNewNodesInEnvWithUnmount,
+ removeNodesWithUnmount,
+ runLifeCycle,
+ setUnmountFuncs,
+} from './mutableHandler.js';
+import { CondNode, VNode } from '../types';
+import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler.js';
+import { startUnmountScope } from '../lifecycle.js';
+import { InulaNodeType } from '@openinula/next-shared';
+
+export function createCondNode(depNum: number, condFunc: (condNode: CondNode) => VNode[]) {
+ startUnmountScope();
+
+ const condNode: CondNode = {
+ __type: InulaNodeType.Cond,
+ cond: -1,
+ didntChange: false,
+ depNum,
+ condFunc,
+ savedContextNodes: getSavedCtxNodes(),
+ _$nodes: [],
+ willUnmountFuncs: [],
+ didUnmountFuncs: [],
+ };
+
+ condNode._$nodes = condFunc(condNode);
+ setUnmountFuncs(condNode);
+
+ if (condNode.willUnmountFuncs) {
+ // ---- Add condNode willUnmount func to the global UnmountStore
+ addWillUnmount(condNode, runLifeCycle.bind(condNode, condNode.willUnmountFuncs));
+ }
+ if (condNode.didUnmountFuncs) {
+ // ---- Add condNode didUnmount func to the global UnmountStore
+ addDidUnmount(condNode, runLifeCycle.bind(condNode, condNode.didUnmountFuncs));
+ }
+
+ return condNode;
+}
+
+/**
+ * @brief the condition changed, update children of current branch
+ */
+export function updateCondChildren(condNode: CondNode, changed: number) {
+ if (condNode.depNum & changed) {
+ // If the depNum of the condition has changed, directly return because node already updated in the `updateBranch`
+ return;
+ }
+ condNode.updateFunc?.(changed);
+}
+
+/**
+ * @brief The update function of CondNode's childNodes when the condition changed
+ */
+export function updateCondNode(condNode: CondNode) {
+ // ---- Need to save prev unmount funcs because we can't put removeNodes before geneNewNodesInEnv
+ // The reason is that if it didn't change, we don't need to unmount or remove the nodes
+ const prevFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs];
+ const newNodes = geneNewNodesInEnvWithUnmount(condNode, () => condNode.condFunc(condNode));
+
+ // ---- If the new nodes are the same as the old nodes, we only need to update children
+ if (condNode.didntChange) {
+ [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs;
+ condNode.didntChange = false;
+ condNode.updateFunc?.(condNode.depNum);
+ return;
+ }
+ // ---- Remove old nodes
+ const newFuncs = [condNode.willUnmountFuncs, condNode.didUnmountFuncs];
+ [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = prevFuncs;
+ condNode._$nodes && condNode._$nodes.length > 0 && removeNodesWithUnmount(condNode, condNode._$nodes);
+ [condNode.willUnmountFuncs, condNode.didUnmountFuncs] = newFuncs;
+
+ if (newNodes.length === 0) {
+ // ---- No branch has been taken
+ condNode._$nodes = [];
+ return;
+ }
+ // ---- Add new nodes
+ const parentEl = condNode._$parentEl!;
+ // ---- Faster append with nextSibling rather than flowIndex
+ const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, condNode);
+
+ const nextSibling = parentEl.childNodes[flowIndex];
+ appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ runDidMount();
+ condNode._$nodes = newNodes;
+}
diff --git a/packages/inula-next/src/MutableNode/ExpNode.js b/packages/inula-next/src/MutableNode/ExpNode.js
deleted file mode 100644
index 0373dc1f..00000000
--- a/packages/inula-next/src/MutableNode/ExpNode.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { DLNodeType } from '../DLNode';
-import { FlatNode } from './FlatNode';
-import { DLStore, cached } from '../store';
-
-export class ExpNode extends FlatNode {
- /**
- * @brief Constructor, Exp type, accept a function that returns a list of nodes
- * @param nodesFunc
- */
- constructor(value, deps) {
- super(DLNodeType.Exp);
- this.initUnmountStore();
- this._$nodes = ExpNode.formatNodes(value);
- this.setUnmountFuncs();
- this.deps = this.parseDeps(deps);
- // ---- Add to the global UnmountStore
- ExpNode.addWillUnmount(this, this.runWillUnmount.bind(this));
- ExpNode.addDidUnmount(this, this.runDidUnmount.bind(this));
- }
-
- parseDeps(deps) {
- return deps.map(dep => {
- // ---- CompNode
- if (dep?.prototype?._$init) return dep.toString();
- // ---- SnippetNode
- if (dep?.propViewFunc) return dep.propViewFunc.toString();
- return dep;
- });
- }
-
- cache(deps) {
- if (!deps || !deps.length) return false;
- deps = this.parseDeps(deps);
- if (cached(deps, this.deps)) return true;
- this.deps = deps;
- return false;
- }
- /**
- * @brief Generate new nodes and replace the old nodes
- */
- update(valueFunc, deps) {
- if (this.cache(deps)) return;
- this.removeNodes(this._$nodes);
- const newNodes = this.geneNewNodesInEnv(() => ExpNode.formatNodes(valueFunc()));
- if (newNodes.length === 0) {
- this._$nodes = [];
- return;
- }
-
- // ---- Add new nodes
- const parentEl = this._$parentEl;
- const flowIndex = ExpNode.getFlowIndexFromNodes(parentEl._$nodes, this);
- const nextSibling = parentEl.childNodes[flowIndex];
- ExpNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
- ExpNode.runDidMount();
-
- this._$nodes = newNodes;
- }
-
- /**
- * @brief Format the nodes
- * @param nodes
- * @returns New nodes
- */
- static formatNodes(nodes) {
- if (!Array.isArray(nodes)) nodes = [nodes];
- return (
- nodes
- // ---- Flatten the nodes
- .flat(1)
- // ---- Filter out empty nodes
- .filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
- .map(node => {
- // ---- If the node is a string, number or bigint, convert it to a text node
- if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
- return DLStore.document.createTextNode(`${node}`);
- }
- // ---- If the node has PropView, call it to get the view
- if ('propViewFunc' in node) return node.build();
- return node;
- })
- // ---- Flatten the nodes again
- .flat(1)
- );
- }
-}
diff --git a/packages/inula-next/src/MutableNode/ExpNode.ts b/packages/inula-next/src/MutableNode/ExpNode.ts
new file mode 100644
index 00000000..6238d23e
--- /dev/null
+++ b/packages/inula-next/src/MutableNode/ExpNode.ts
@@ -0,0 +1,84 @@
+import { InulaNodeType } from '@openinula/next-shared';
+import { appendNodesWithSibling, getFlowIndexFromNodes } from '../InulaNode';
+import { addDidUnmount, addWillUnmount, runDidMount } from '../lifecycle';
+import { equal } from '../equal';
+import { ChildrenNode, ExpNode, VNode, TextNode, InulaNode } from '../types';
+import { removeNodesWithUnmount, runLifeCycle, setUnmountFuncs } from './mutableHandler';
+import { geneNewNodesInCtx, getSavedCtxNodes, removeNodes } from './mutableHandler';
+import { startUnmountScope } from '../lifecycle';
+import { buildChildren } from '../ChildrenNode';
+import { createTextNode } from '../renderer/dom';
+
+function isChildrenNode(node: any): node is ChildrenNode {
+ return node.__type === InulaNodeType.Children;
+}
+function getExpressionResult(fn: () => Array) {
+ let nodes = fn();
+ if (!Array.isArray(nodes)) nodes = [nodes];
+ return (
+ nodes
+ // ---- Flatten the nodes
+ .flat(1)
+ // ---- Filter out empty nodes
+ .filter(node => node !== undefined && node !== null && typeof node !== 'boolean')
+ .map(node => {
+ // ---- If the node is a string, number or bigint, convert it to a text node
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'bigint') {
+ return createTextNode(`${node}`);
+ }
+ // TODO ---- If the node has PropView, call it to get the view,
+ if (isChildrenNode(node)) return buildChildren(node);
+ return node;
+ })
+ // ---- Flatten the nodes again
+ .flat(1)
+ );
+}
+export function createExpNode(value: () => VNode[], deps: unknown[]) {
+ startUnmountScope();
+
+ const expNode: ExpNode = {
+ __type: InulaNodeType.Exp,
+ _$nodes: getExpressionResult(value),
+ deps,
+ savedContextNodes: getSavedCtxNodes(),
+ willUnmountFuncs: [],
+ didUnmountFuncs: [],
+ };
+
+ setUnmountFuncs(expNode);
+
+ if (expNode.willUnmountFuncs) {
+ // ---- Add expNode willUnmount func to the global UnmountStore
+ addWillUnmount(expNode, runLifeCycle.bind(expNode, expNode.willUnmountFuncs));
+ }
+ if (expNode.didUnmountFuncs) {
+ // ---- Add expNode didUnmount func to the global UnmountStore
+ addDidUnmount(expNode, runLifeCycle.bind(expNode, expNode.didUnmountFuncs));
+ }
+ return expNode;
+}
+
+export function updateExpNode(expNode: ExpNode, valueFunc: () => VNode[], deps: unknown[]) {
+ if (cache(expNode, deps)) return;
+ removeNodesWithUnmount(expNode, expNode._$nodes);
+ const newNodes = geneNewNodesInCtx(expNode, () => getExpressionResult(valueFunc));
+ if (newNodes.length === 0) {
+ expNode._$nodes = [];
+ return;
+ }
+ const parentEl = expNode._$parentEl!;
+ const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, expNode);
+ const nextSibling = parentEl.childNodes[flowIndex];
+ appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ runDidMount();
+
+ expNode._$nodes = newNodes;
+}
+
+function cache(expNode: ExpNode, deps: unknown[]) {
+ if (!deps || !deps.length) return false;
+ if (equal(deps, expNode.deps)) return true;
+ expNode.deps = deps;
+ return false;
+}
diff --git a/packages/inula-next/src/MutableNode/FlatNode.js b/packages/inula-next/src/MutableNode/FlatNode.js
deleted file mode 100644
index 9f1519e8..00000000
--- a/packages/inula-next/src/MutableNode/FlatNode.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { DLStore } from '../store';
-import { MutableNode } from './MutableNode';
-
-export class FlatNode extends MutableNode {
- willUnmountFuncs = [];
- didUnmountFuncs = [];
-
- setUnmountFuncs() {
- this.willUnmountFuncs = DLStore.global.WillUnmountStore.pop();
- this.didUnmountFuncs = DLStore.global.DidUnmountStore.pop();
- }
-
- runWillUnmount() {
- for (let i = 0; i < this.willUnmountFuncs.length; i++) this.willUnmountFuncs[i]();
- }
-
- runDidUnmount() {
- for (let i = this.didUnmountFuncs.length - 1; i >= 0; i--) this.didUnmountFuncs[i]();
- }
-
- removeNodes(nodes) {
- this.runWillUnmount();
- super.removeNodes(nodes);
- this.runDidUnmount();
- }
-
- geneNewNodesInEnv(newNodesFunc) {
- this.initUnmountStore();
- const nodes = super.geneNewNodesInEnv(newNodesFunc);
- this.setUnmountFuncs();
- return nodes;
- }
-}
diff --git a/packages/inula-next/src/MutableNode/ForNode.js b/packages/inula-next/src/MutableNode/ForNode.js
deleted file mode 100644
index 0cc14972..00000000
--- a/packages/inula-next/src/MutableNode/ForNode.js
+++ /dev/null
@@ -1,406 +0,0 @@
-import { DLNodeType } from '../DLNode';
-import { DLStore } from '../store';
-import { MutableNode } from './MutableNode';
-
-export class ForNode extends MutableNode {
- array;
- nodeFunc;
- depNum;
-
- nodesMap = new Map();
- updateArr = [];
-
- /**
- * @brief Getter for nodes
- */
- get _$nodes() {
- const nodes = [];
- for (let idx = 0; idx < this.array.length; idx++) {
- nodes.push(...this.nodesMap.get(this.keys?.[idx] ?? idx));
- }
- return nodes;
- }
-
- /**
- * @brief Constructor, For type
- * @param array
- * @param nodeFunc
- * @param keys
- */
- constructor(array, depNum, keys, nodeFunc) {
- super(DLNodeType.For);
- this.array = [...array];
- this.keys = keys;
- this.depNum = depNum;
- this.addNodeFunc(nodeFunc);
- }
-
- /**
- * @brief To be called immediately after the constructor
- * @param nodeFunc
- */
- addNodeFunc(nodeFunc) {
- this.nodeFunc = nodeFunc;
- this.array.forEach((item, idx) => {
- this.initUnmountStore();
- const key = this.keys?.[idx] ?? idx;
- const nodes = nodeFunc(item, this.updateArr, idx);
- this.nodesMap.set(key, nodes);
- this.setUnmountMap(key);
- });
- // ---- For nested ForNode, the whole strategy is just like EnvStore
- // we use array of function array to create "environment", popping and pushing
- ForNode.addWillUnmount(this, this.runAllWillUnmount.bind(this));
- ForNode.addDidUnmount(this, this.runAllDidUnmount.bind(this));
- }
-
- /**
- * @brief Update the view related to one item in the array
- * @param nodes
- * @param item
- */
- updateItem(idx, array, changed) {
- // ---- The update function of ForNode's childNodes is stored in the first child node
- this.updateArr[idx]?.(changed ?? this.depNum, array[idx]);
- }
-
- updateItems(changed) {
- for (let idx = 0; idx < this.array.length; idx++) {
- this.updateItem(idx, this.array, changed);
- }
- }
-
- /**
- * @brief Non-array update function
- * @param changed
- */
- update(changed) {
- // ---- e.g. this.depNum -> 1110 changed-> 1010
- // ~this.depNum & changed -> ~1110 & 1010 -> 0000
- // no update because depNum contains all the changed
- // ---- e.g. this.depNum -> 1110 changed-> 1101
- // ~this.depNum & changed -> ~1110 & 1101 -> 0001
- // update because depNum doesn't contain all the changed
- if (!(~this.depNum & changed)) return;
- this.updateItems(changed);
- }
-
- /**
- * @brief Array-related update function
- * @param newArray
- * @param newKeys
- */
- updateArray(newArray, newKeys) {
- if (newKeys) {
- this.updateWithKey(newArray, newKeys);
- return;
- }
- this.updateWithOutKey(newArray);
- }
-
- /**
- * @brief Shortcut to generate new nodes with idx and key
- */
- getNewNodes(idx, key, array, updateArr) {
- this.initUnmountStore();
- const nodes = this.geneNewNodesInEnv(() => this.nodeFunc(array[idx], updateArr ?? this.updateArr, idx));
- this.setUnmountMap(key);
- this.nodesMap.set(key, nodes);
- return nodes;
- }
-
- /**
- * @brief Set the unmount map by getting the last unmount map from the global store
- * @param key
- */
- setUnmountMap(key) {
- const willUnmountMap = DLStore.global.WillUnmountStore.pop();
- if (willUnmountMap && willUnmountMap.length > 0) {
- if (!this.willUnmountMap) this.willUnmountMap = new Map();
- this.willUnmountMap.set(key, willUnmountMap);
- }
- const didUnmountMap = DLStore.global.DidUnmountStore.pop();
- if (didUnmountMap && didUnmountMap.length > 0) {
- if (!this.didUnmountMap) this.didUnmountMap = new Map();
- this.didUnmountMap.set(key, didUnmountMap);
- }
- }
-
- /**
- * @brief Run all the unmount functions and clear the unmount map
- */
- runAllWillUnmount() {
- if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
- this.willUnmountMap.forEach(funcs => {
- for (let i = 0; i < funcs.length; i++) funcs[i]?.();
- });
- this.willUnmountMap.clear();
- }
-
- /**
- * @brief Run all the unmount functions and clear the unmount map
- */
- runAllDidUnmount() {
- if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
- this.didUnmountMap.forEach(funcs => {
- for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
- });
- this.didUnmountMap.clear();
- }
-
- /**
- * @brief Run the unmount functions of the given key
- * @param key
- */
- runWillUnmount(key) {
- if (!this.willUnmountMap || this.willUnmountMap.size === 0) return;
- const funcs = this.willUnmountMap.get(key);
- if (!funcs) return;
- for (let i = 0; i < funcs.length; i++) funcs[i]?.();
- this.willUnmountMap.delete(key);
- }
-
- /**
- * @brief Run the unmount functions of the given key
- */
- runDidUnmount(key) {
- if (!this.didUnmountMap || this.didUnmountMap.size === 0) return;
- const funcs = this.didUnmountMap.get(key);
- if (!funcs) return;
- for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
- this.didUnmountMap.delete(key);
- }
-
- /**
- * @brief Remove nodes from parentEl and run willUnmount and didUnmount
- * @param nodes
- * @param key
- */
- removeNodes(nodes, key) {
- this.runWillUnmount(key);
- super.removeNodes(nodes);
- this.runDidUnmount(key);
- this.nodesMap.delete(key);
- }
-
- /**
- * @brief Update the nodes without keys
- * @param newArray
- */
- updateWithOutKey(newArray) {
- const preLength = this.array.length;
- const currLength = newArray.length;
-
- if (preLength === currLength) {
- // ---- If the length is the same, we only need to update the nodes
- for (let idx = 0; idx < this.array.length; idx++) {
- this.updateItem(idx, newArray);
- }
- this.array = [...newArray];
- return;
- }
- const parentEl = this._$parentEl;
- // ---- If the new array is longer, add new nodes directly
- if (preLength < currLength) {
- let flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
- // ---- Calling parentEl.childNodes.length is time-consuming,
- // so we use a length variable to store the length
- const length = parentEl.childNodes.length;
- for (let idx = 0; idx < currLength; idx++) {
- if (idx < preLength) {
- flowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(idx));
- this.updateItem(idx, newArray);
- continue;
- }
- const newNodes = this.getNewNodes(idx, idx, newArray);
- ForNode.appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
- }
- ForNode.runDidMount();
- this.array = [...newArray];
- return;
- }
-
- // ---- Update the nodes first
- for (let idx = 0; idx < currLength; idx++) {
- this.updateItem(idx, newArray);
- }
- // ---- If the new array is shorter, remove the extra nodes
- for (let idx = currLength; idx < preLength; idx++) {
- const nodes = this.nodesMap.get(idx);
- this.removeNodes(nodes, idx);
- }
- this.updateArr.splice(currLength, preLength - currLength);
- this.array = [...newArray];
- }
-
- /**
- * @brief Update the nodes with keys
- * @param newArray
- * @param newKeys
- */
- updateWithKey(newArray, newKeys) {
- if (newKeys.length !== new Set(newKeys).size) {
- throw new Error('DLight: Duplicate keys in for loop are not allowed');
- }
- const prevKeys = this.keys;
- this.keys = newKeys;
-
- if (ForNode.arrayEqual(prevKeys, this.keys)) {
- // ---- If the keys are the same, we only need to update the nodes
- for (let idx = 0; idx < newArray.length; idx++) {
- this.updateItem(idx, newArray);
- }
- this.array = [...newArray];
- return;
- }
-
- const parentEl = this._$parentEl;
-
- // ---- No nodes after, delete all nodes
- if (this.keys.length === 0) {
- const parentNodes = parentEl._$nodes ?? [];
- if (parentNodes.length === 1 && parentNodes[0] === this) {
- // ---- ForNode is the only node in the parent node
- // Frequently used in real life scenarios because we tend to always wrap for with a div element,
- // so we optimize it here
- this.runAllWillUnmount();
- parentEl.innerHTML = '';
- this.runAllDidUnmount();
- } else {
- for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
- const prevKey = prevKeys[prevIdx];
- this.removeNodes(this.nodesMap.get(prevKey), prevKey);
- }
- }
- this.nodesMap.clear();
- this.updateArr = [];
- this.array = [];
- return;
- }
-
- // ---- Record how many nodes are before this ForNode with the same parentNode
- const flowIndex = ForNode.getFlowIndexFromNodes(parentEl._$nodes, this);
-
- // ---- No nodes before, append all nodes
- if (prevKeys.length === 0) {
- const nextSibling = parentEl.childNodes[flowIndex];
- for (let idx = 0; idx < this.keys.length; idx++) {
- const newNodes = this.getNewNodes(idx, this.keys[idx], newArray);
- ForNode.appendNodesWithSibling(newNodes, parentEl, nextSibling);
- }
- ForNode.runDidMount();
- this.array = [...newArray];
- return;
- }
-
- const shuffleKeys = [];
- const newUpdateArr = [];
-
- // ---- 1. Delete the nodes that are no longer in the array
- for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
- const prevKey = prevKeys[prevIdx];
- if (this.keys.includes(prevKey)) {
- shuffleKeys.push(prevKey);
- newUpdateArr.push(this.updateArr[prevIdx]);
- continue;
- }
- this.removeNodes(this.nodesMap.get(prevKey), prevKey);
- }
-
- // ---- 2. Add the nodes that are not in the array but in the new array
- // ---- Calling parentEl.childNodes.length is time-consuming,
- // so we use a length variable to store the length
- let length = parentEl.childNodes.length;
- let newFlowIndex = flowIndex;
- for (let idx = 0; idx < this.keys.length; idx++) {
- const key = this.keys[idx];
- const prevIdx = shuffleKeys.indexOf(key);
- if (prevIdx !== -1) {
- // ---- These nodes are already in the parentEl,
- // and we need to keep track of their flowIndex
- newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
- newUpdateArr[prevIdx]?.(this.depNum, newArray[idx]);
- continue;
- }
- // ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
- newUpdateArr.splice(idx, 0, null);
- const newNodes = this.getNewNodes(idx, key, newArray, newUpdateArr);
- // ---- Add the new nodes
- shuffleKeys.splice(idx, 0, key);
-
- const count = ForNode.appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
- newFlowIndex += count;
- length += count;
- }
- ForNode.runDidMount();
-
- // ---- After adding and deleting, the only thing left is to reorder the nodes,
- // but if the keys are the same, we don't need to reorder
- if (ForNode.arrayEqual(this.keys, shuffleKeys)) {
- this.array = [...newArray];
- this.updateArr = newUpdateArr;
- return;
- }
-
- newFlowIndex = flowIndex;
- const bufferNodes = new Map();
- // ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
- for (let idx = 0; idx < this.keys.length; idx++) {
- const key = this.keys[idx];
- const prevIdx = shuffleKeys.indexOf(key);
-
- const bufferedNode = bufferNodes.get(key);
- if (bufferedNode) {
- // ---- We need to add the flowIndex of the bufferedNode,
- // because the bufferedNode is in the parentEl and the new position is ahead of the previous position
- const bufferedFlowIndex = ForNode.getFlowIndexFromNodes(bufferedNode);
- const lastEl = ForNode.toEls(bufferedNode).pop();
- const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
- if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
- // ---- If the node is buffered, we need to add it to the parentEl
- ForNode.insertNodesBefore(bufferedNode, parentEl, nextSibling);
- }
- // ---- So the added length is the length of the bufferedNode
- newFlowIndex += bufferedFlowIndex;
- delete bufferNodes[idx];
- } else if (prevIdx === idx) {
- // ---- If the node is in the same position, we don't need to do anything
- newFlowIndex += ForNode.getFlowIndexFromNodes(this.nodesMap.get(key));
- continue;
- } else {
- // ---- If the node is not in the same position, we need to buffer it
- // We buffer the node of the previous position, and then replace it with the node of the current position
- const prevKey = shuffleKeys[idx];
- bufferNodes.set(prevKey, this.nodesMap.get(prevKey));
- // ---- Length would never change, and the last will always be in the same position,
- // so it'll always be insertBefore instead of appendChild
- const childNodes = this.nodesMap.get(key);
- const lastEl = ForNode.toEls(childNodes).pop();
- const nextSibling = parentEl.childNodes[newFlowIndex];
- if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
- newFlowIndex += ForNode.insertNodesBefore(childNodes, parentEl, nextSibling);
- }
- }
- // ---- Swap the keys
- const tempKey = shuffleKeys[idx];
- shuffleKeys[idx] = shuffleKeys[prevIdx];
- shuffleKeys[prevIdx] = tempKey;
- const tempUpdateFunc = newUpdateArr[idx];
- newUpdateArr[idx] = newUpdateArr[prevIdx];
- newUpdateArr[prevIdx] = tempUpdateFunc;
- }
- this.array = [...newArray];
- this.updateArr = newUpdateArr;
- }
-
- /**
- * @brief Compare two arrays
- * @param arr1
- * @param arr2
- * @returns
- */
- static arrayEqual(arr1, arr2) {
- if (arr1.length !== arr2.length) return false;
- return arr1.every((item, idx) => item === arr2[idx]);
- }
-}
diff --git a/packages/inula-next/src/MutableNode/ForNode.ts b/packages/inula-next/src/MutableNode/ForNode.ts
new file mode 100644
index 00000000..301173c5
--- /dev/null
+++ b/packages/inula-next/src/MutableNode/ForNode.ts
@@ -0,0 +1,373 @@
+import { InulaNodeType } from '@openinula/next-shared';
+import {
+ appendNodesWithIndex,
+ appendNodesWithSibling,
+ getFlowIndexFromNodes,
+ insertNodesBefore,
+ toEls,
+} from '../InulaNode';
+import { addDidUnmount, addWillUnmount, endUnmountScope, runDidMount } from '../lifecycle';
+import { ForNode, InulaNode, VNode } from '../types';
+import { geneNewNodesInCtx, getSavedCtxNodes } from './mutableHandler';
+import { startUnmountScope } from '../lifecycle';
+import { removeNodes as removeMutableNodes } from './mutableHandler';
+
+export function createForNode(
+ array: T[],
+ depNum: number,
+ keys: number[],
+ nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]
+) {
+ const forNode: ForNode = {
+ __type: InulaNodeType.For,
+ array: [...array],
+ depNum,
+ keys,
+ nodeFunc,
+ nodesMap: new Map(),
+ updateArr: [],
+ didUnmountFuncs: new Map(),
+ willUnmountFuncs: new Map(),
+ savedContextNodes: getSavedCtxNodes(),
+ get _$nodes() {
+ const nodes = [];
+ for (let idx = 0; idx < forNode.array.length; idx++) {
+ nodes.push(...forNode.nodesMap.get(forNode.keys?.[idx] ?? idx)!);
+ }
+ return nodes;
+ },
+ };
+ addNodeFunc(forNode, nodeFunc);
+ return forNode;
+}
+
+/**
+ * @brief To be called immediately after the constructor
+ * @param forNode
+ * @param nodeFunc
+ */
+function addNodeFunc(forNode: ForNode, nodeFunc: (item: T, idx: number, updateArr: any[]) => VNode[]) {
+ forNode.array.forEach((item, idx) => {
+ startUnmountScope();
+ const key = forNode.keys?.[idx] ?? idx;
+ const nodes = nodeFunc(item, idx, forNode.updateArr);
+ forNode.nodesMap.set(key, nodes);
+ setUnmountMap(forNode, key);
+ });
+
+ // ---- For nested ForNode, the whole strategy is just like EnvStore
+ // we use array of function array to create "environment", popping and pushing
+ addWillUnmount(forNode, () => runLifecycleMap(forNode.willUnmountFuncs));
+ addDidUnmount(forNode, () => runLifecycleMap(forNode.didUnmountFuncs));
+}
+
+function runLifecycleMap(map: Map, key?: number) {
+ if (!map || map.size === 0) {
+ return;
+ }
+ if (typeof key === 'number') {
+ const funcs = map.get(key);
+ if (!funcs) return;
+ for (let i = 0; i < funcs.length; i++) funcs[i]?.();
+ map.delete(key);
+ } else {
+ map.forEach(funcs => {
+ for (let i = funcs.length - 1; i >= 0; i--) funcs[i]?.();
+ });
+ map.clear();
+ }
+}
+
+/**
+ * @brief Set the unmount map by getting the last unmount map from the global store
+ * @param key
+ */
+function setUnmountMap(forNode: ForNode, key: number) {
+ const [willUnmountMap, didUnmountMap] = endUnmountScope();
+ if (willUnmountMap && willUnmountMap.length > 0) {
+ if (!forNode.willUnmountFuncs) forNode.willUnmountFuncs = new Map();
+ forNode.willUnmountFuncs.set(key, willUnmountMap);
+ }
+ if (didUnmountMap && didUnmountMap.length > 0) {
+ if (!forNode.didUnmountFuncs) forNode.didUnmountFuncs = new Map();
+ forNode.didUnmountFuncs.set(key, didUnmountMap);
+ }
+}
+
+/**
+ * @brief Non-array update function, invoke children's update function
+ * @param changed
+ */
+export function updateForChildren(forNode: ForNode, changed: number) {
+ // ---- e.g. this.depNum -> 1110 changed-> 1010
+ // ~this.depNum & changed -> ~1110 & 1010 -> 0000
+ // no update because depNum contains all the changed
+ // ---- e.g. this.depNum -> 1110 changed-> 1101
+ // ~this.depNum & changed -> ~1110 & 1101 -> 000f1
+ // update because depNum doesn't contain all the changed
+ if (!(~forNode.depNum & changed)) return;
+ for (let idx = 0; idx < forNode.array.length; idx++) {
+ updateItem(forNode, idx, forNode.array, changed);
+ }
+}
+
+/**
+ * @brief Update the view related to one item in the array
+ * @param forNode - The ForNode
+ * @param idx - The index of the item in the array
+ * @param array - The array of items
+ * @param changed - The changed bit
+ */
+function updateItem(forNode: ForNode, idx: number, array: T[], changed?: number) {
+ // ---- The update function of ForNode's childNodes is stored in the first child node
+ forNode.updateArr[idx]?.(changed ?? forNode.depNum, array[idx]);
+}
+
+/**
+ * @brief Array-related update function
+ */
+export function updateForNode(forNode: ForNode, newArray: T[], newKeys: number[]) {
+ if (newKeys) {
+ updateWithKey(forNode, newArray, newKeys);
+ return;
+ }
+ updateWithOutKey(forNode, newArray);
+}
+
+/**
+ * @brief Shortcut to generate new nodes with idx and key
+ */
+function getNewNodes(forNode: ForNode, idx: number, key: number, array: T[], updateArr?: any[]) {
+ startUnmountScope();
+ const nodes = geneNewNodesInCtx(forNode, () => forNode.nodeFunc(array[idx], idx, updateArr ?? forNode.updateArr));
+ setUnmountMap(forNode, key);
+ forNode.nodesMap.set(key, nodes);
+ return nodes;
+}
+
+/**
+ * @brief Remove nodes from parentEl and run willUnmount and didUnmount
+ * @param nodes
+ * @param key
+ */
+function removeNodes(forNode: ForNode, nodes: InulaNode[], key: number) {
+ runLifecycleMap(forNode.willUnmountFuncs, key);
+ removeMutableNodes(forNode, nodes);
+ runLifecycleMap(forNode.didUnmountFuncs, key);
+ forNode.nodesMap.delete(key);
+}
+
+/**
+ * @brief Update the nodes without keys
+ * @param newArray
+ */
+function updateWithOutKey(forNode: ForNode, newArray: T[]) {
+ const preLength = forNode.array.length;
+ const currLength = newArray.length;
+
+ if (preLength === currLength) {
+ // ---- If the length is the same, we only need to update the nodes
+ for (let idx = 0; idx < forNode.array.length; idx++) {
+ updateItem(forNode, idx, newArray);
+ }
+ forNode.array = [...newArray];
+ return;
+ }
+ const parentEl = forNode._$parentEl!;
+ // ---- If the new array is longer, add new nodes directly
+ if (preLength < currLength) {
+ let flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode);
+ // ---- Calling parentEl.childNodes.length is time-consuming,
+ // so we use a length variable to store the length
+ const length = parentEl.childNodes.length;
+ for (let idx = 0; idx < currLength; idx++) {
+ if (idx < preLength) {
+ flowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(idx)!);
+ updateItem(forNode, idx, newArray);
+ continue;
+ }
+ const newNodes = getNewNodes(forNode, idx, idx, newArray);
+ appendNodesWithIndex(newNodes, parentEl, flowIndex, length);
+ }
+ runDidMount();
+ forNode.array = [...newArray];
+ return;
+ }
+
+ // ---- Update the nodes first
+ for (let idx = 0; idx < currLength; idx++) {
+ updateItem(forNode, idx, newArray);
+ }
+ // ---- If the new array is shorter, remove the extra nodes
+ for (let idx = currLength; idx < preLength; idx++) {
+ const nodes = forNode.nodesMap.get(idx);
+ removeNodes(forNode, nodes!, idx);
+ }
+ forNode.updateArr.splice(currLength, preLength - currLength);
+ forNode.array = [...newArray];
+}
+
+function arrayEqual(arr1: T[], arr2: T[]) {
+ if (arr1.length !== arr2.length) return false;
+ return arr1.every((item, idx) => item === arr2[idx]);
+}
+
+/**
+ * @brief Update the nodes with keys
+ * @param newArray
+ * @param newKeys
+ */
+function updateWithKey(forNode: ForNode, newArray: T[], newKeys: number[]) {
+ if (newKeys.length !== new Set(newKeys).size) {
+ throw new Error('Inula-Next: Duplicate keys in for loop are not allowed');
+ }
+ const prevKeys = forNode.keys;
+ forNode.keys = newKeys;
+
+ if (arrayEqual(prevKeys, newKeys)) {
+ // ---- If the keys are the same, we only need to update the nodes
+ for (let idx = 0; idx < newArray.length; idx++) {
+ updateItem(forNode, idx, newArray);
+ }
+ forNode.array = [...newArray];
+ return;
+ }
+
+ const parentEl = forNode._$parentEl!;
+
+ // ---- No nodes after, delete all nodes
+ if (newKeys.length === 0) {
+ const parentNodes = parentEl._$nodes ?? [];
+ if (parentNodes.length === 1 && parentNodes[0] === forNode) {
+ // ---- ForNode is the only node in the parent node
+ // Frequently used in real life scenarios because we tend to always wrap for with a div element,
+ // so we optimize it here
+ runLifecycleMap(forNode.willUnmountFuncs);
+ parentEl.innerHTML = '';
+ runLifecycleMap(forNode.didUnmountFuncs);
+ } else {
+ for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
+ const prevKey = prevKeys[prevIdx];
+ removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey);
+ }
+ }
+ forNode.nodesMap.clear();
+ forNode.updateArr = [];
+ forNode.array = [];
+ return;
+ }
+
+ // ---- Record how many nodes are before this ForNode with the same parentNode
+ const flowIndex = getFlowIndexFromNodes(parentEl._$nodes, forNode);
+
+ // ---- No nodes before, append all nodes
+ if (prevKeys.length === 0) {
+ const nextSibling = parentEl.childNodes[flowIndex];
+ for (let idx = 0; idx < newKeys.length; idx++) {
+ const newNodes = getNewNodes(forNode, idx, newKeys[idx], newArray);
+ appendNodesWithSibling(newNodes, parentEl, nextSibling);
+ }
+ runDidMount();
+ forNode.array = [...newArray];
+ return;
+ }
+
+ const shuffleKeys: number[] = [];
+ const newUpdateArr: any[] = [];
+
+ // ---- 1. Delete the nodes that are no longer in the array
+ for (let prevIdx = 0; prevIdx < prevKeys.length; prevIdx++) {
+ const prevKey = prevKeys[prevIdx];
+ if (forNode.keys.includes(prevKey)) {
+ shuffleKeys.push(prevKey);
+ newUpdateArr.push(forNode.updateArr[prevIdx]);
+ continue;
+ }
+ removeNodes(forNode, forNode.nodesMap.get(prevKey)!, prevKey);
+ }
+
+ // ---- 2. Add the nodes that are not in the array but in the new array
+ // ---- Calling parentEl.childNodes.length is time-consuming,
+ // so we use a length variable to store the length
+ let length = parentEl.childNodes.length;
+ let newFlowIndex = flowIndex;
+ for (let idx = 0; idx < forNode.keys.length; idx++) {
+ const key = forNode.keys[idx];
+ const prevIdx = shuffleKeys.indexOf(key);
+ if (prevIdx !== -1) {
+ // ---- These nodes are already in the parentEl,
+ // and we need to keep track of their flowIndex
+ newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!);
+ newUpdateArr[prevIdx]?.(forNode.depNum, newArray[idx]);
+ continue;
+ }
+ // ---- Insert updateArr first because in getNewNode the updateFunc will replace this null
+ newUpdateArr.splice(idx, 0, null);
+ const newNodes = getNewNodes(forNode, idx, key, newArray);
+ // ---- Add the new nodes
+ shuffleKeys.splice(idx, 0, key);
+
+ const count = appendNodesWithIndex(newNodes, parentEl, newFlowIndex, length);
+ newFlowIndex += count;
+ length += count;
+ }
+ runDidMount();
+
+ // ---- After adding and deleting, the only thing left is to reorder the nodes,
+ // but if the keys are the same, we don't need to reorder
+ if (arrayEqual(forNode.keys, shuffleKeys)) {
+ forNode.array = [...newArray];
+ forNode.updateArr = newUpdateArr;
+ return;
+ }
+
+ newFlowIndex = flowIndex;
+ const bufferNodes = new Map();
+ // ---- 3. Replace the nodes in the same position using Fisher-Yates shuffle algorithm
+ for (let idx = 0; idx < forNode.keys.length; idx++) {
+ const key = forNode.keys[idx];
+ const prevIdx = shuffleKeys.indexOf(key);
+
+ const bufferedNode = bufferNodes.get(key);
+ if (bufferedNode) {
+ // ---- We need to add the flowIndex of the bufferedNode,
+ // because the bufferedNode is in the parentEl and the new position is ahead of the previous position
+ const bufferedFlowIndex = getFlowIndexFromNodes(bufferedNode);
+ const lastEl = toEls(bufferedNode).pop()!;
+ const nextSibling = parentEl.childNodes[newFlowIndex + bufferedFlowIndex];
+ if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
+ // ---- If the node is buffered, we need to add it to the parentEl
+ insertNodesBefore(bufferedNode, parentEl, nextSibling);
+ }
+ // ---- So the added length is the length of the bufferedNode
+ newFlowIndex += bufferedFlowIndex;
+ // TODO: ?? delete bufferNodes[idx];
+ } else if (prevIdx === idx) {
+ // ---- If the node is in the same position, we don't need to do anything
+ newFlowIndex += getFlowIndexFromNodes(forNode.nodesMap.get(key)!);
+ continue;
+ } else {
+ // ---- If the node is not in the same position, we need to buffer it
+ // We buffer the node of the previous position, and then replace it with the node of the current position
+ const prevKey = shuffleKeys[idx];
+ bufferNodes.set(prevKey, forNode.nodesMap.get(prevKey)!);
+ // ---- Length would never change, and the last will always be in the same position,
+ // so it'll always be insertBefore instead of appendChild
+ const childNodes = forNode.nodesMap.get(key)!;
+ const lastEl = toEls(childNodes).pop()!;
+ const nextSibling = parentEl.childNodes[newFlowIndex];
+ if (lastEl !== nextSibling && lastEl.nextSibling !== nextSibling) {
+ newFlowIndex += insertNodesBefore(childNodes, parentEl, nextSibling);
+ }
+ }
+ // ---- Swap the keys
+ const tempKey = shuffleKeys[idx];
+ shuffleKeys[idx] = shuffleKeys[prevIdx];
+ shuffleKeys[prevIdx] = tempKey;
+ const tempUpdateFunc = newUpdateArr[idx];
+ newUpdateArr[idx] = newUpdateArr[prevIdx];
+ newUpdateArr[prevIdx] = tempUpdateFunc;
+ }
+ forNode.array = [...newArray];
+ forNode.updateArr = newUpdateArr;
+}
diff --git a/packages/inula-next/src/MutableNode/MutableNode.js b/packages/inula-next/src/MutableNode/MutableNode.js
deleted file mode 100644
index b557bdbc..00000000
--- a/packages/inula-next/src/MutableNode/MutableNode.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import { DLNode } from '../DLNode';
-import { DLStore } from '../store';
-
-export class MutableNode extends DLNode {
- /**
- * @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
- * 1. The environment of the new nodes should be the same as the old nodes
- * 2. The new nodes should be added to the parentEl
- * 3. The old nodes should be removed from the parentEl
- * @param type
- */
- constructor(type) {
- super(type);
- // ---- Save the current environment nodes, must be a new reference
- if (DLStore.global.DLEnvStore && DLStore.global.DLEnvStore.currentEnvNodes.length > 0) {
- this.savedEnvNodes = [...DLStore.global.DLEnvStore.currentEnvNodes];
- }
- }
-
- /**
- * @brief Initialize the new nodes, add parentEl to all nodes
- * @param nodes
- */
- initNewNodes(nodes) {
- // ---- Add parentEl to all nodes
- DLNode.addParentEl(nodes, this._$parentEl);
- }
-
- /**
- * @brief Generate new nodes in the saved environment
- * @param newNodesFunc
- * @returns
- */
- geneNewNodesInEnv(newNodesFunc) {
- if (!this.savedEnvNodes) {
- // ---- No saved environment, just generate new nodes
- const newNodes = newNodesFunc();
- // ---- Only for IfNode's same condition return
- // ---- Initialize the new nodes
- this.initNewNodes(newNodes);
- return newNodes;
- }
- // ---- Save the current environment nodes
- const currentEnvNodes = DLStore.global.DLEnvStore.currentEnvNodes;
- // ---- Replace the saved environment nodes
- DLStore.global.DLEnvStore.replaceEnvNodes(this.savedEnvNodes);
- const newNodes = newNodesFunc();
- // ---- Retrieve the current environment nodes
- DLStore.global.DLEnvStore.replaceEnvNodes(currentEnvNodes);
- // ---- Only for IfNode's same condition return
- // ---- Initialize the new nodes
- this.initNewNodes(newNodes);
- return newNodes;
- }
-
- initUnmountStore() {
- DLStore.global.WillUnmountStore.push([]);
- DLStore.global.DidUnmountStore.push([]);
- }
-
- /**
- * @brief Remove nodes from parentEl and run willUnmount and didUnmount
- * @param nodes
- * @param removeEl Only remove outermost element
- */
- removeNodes(nodes) {
- DLNode.loopShallowEls(nodes, node => {
- this._$parentEl.removeChild(node);
- });
- }
-}
diff --git a/packages/inula-next/src/MutableNode/TryNode.js b/packages/inula-next/src/MutableNode/TryNode.js
deleted file mode 100644
index 12f1bb18..00000000
--- a/packages/inula-next/src/MutableNode/TryNode.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { DLNodeType } from '../DLNode';
-import { FlatNode } from './FlatNode';
-import { EnvNode } from '../EnvNode';
-
-export class TryNode extends FlatNode {
- constructor(tryFunc, catchFunc) {
- super(DLNodeType.Try);
- this.tryFunc = tryFunc;
- const catchable = this.getCatchable(catchFunc);
- this.envNode = new EnvNode({ _$catchable: catchable });
- const nodes = tryFunc(this.setUpdateFunc.bind(this), catchable) ?? [];
- this.envNode.initNodes(nodes);
- this._$nodes = nodes;
- }
-
- update(changed) {
- this.updateFunc?.(changed);
- }
-
- setUpdateFunc(updateFunc) {
- this.updateFunc = updateFunc;
- }
-
- getCatchable(catchFunc) {
- return callback =>
- (...args) => {
- try {
- return callback(...args);
- } catch (e) {
- // ---- Run it in next tick to make sure when error occurs before
- // didMount, this._$parentEl is not null
- Promise.resolve().then(() => {
- const nodes = this.geneNewNodesInEnv(() => catchFunc(this.setUpdateFunc.bind(this), e));
- this._$nodes && this.removeNodes(this._$nodes);
- const parentEl = this._$parentEl;
- const flowIndex = FlatNode.getFlowIndexFromNodes(parentEl._$nodes, this);
- const nextSibling = parentEl.childNodes[flowIndex];
- FlatNode.appendNodesWithSibling(nodes, parentEl, nextSibling);
- FlatNode.runDidMount();
- this._$nodes = nodes;
- });
- }
- };
- }
-}
diff --git a/packages/inula-next/src/MutableNode/mutableHandler.ts b/packages/inula-next/src/MutableNode/mutableHandler.ts
new file mode 100644
index 00000000..b2e00b65
--- /dev/null
+++ b/packages/inula-next/src/MutableNode/mutableHandler.ts
@@ -0,0 +1,81 @@
+import { MutableNode, VNode, ScopedLifecycle, InulaNode, ContextNode } from '../types';
+import { endUnmountScope, startUnmountScope } from '../lifecycle';
+import { getContextNodeMap, replaceContext } from '../ContextNode';
+import { addParentEl, loopShallowEls } from '../InulaNode';
+
+export function setUnmountFuncs(node: MutableNode) {
+ // pop will not be undefined,cause we push empty array when create node
+ const [willUnmountFuncs, didUnmountFuncs] = endUnmountScope();
+ node.willUnmountFuncs = willUnmountFuncs!;
+ node.didUnmountFuncs = didUnmountFuncs!;
+}
+
+export function runLifeCycle(fn: ScopedLifecycle) {
+ for (let i = 0; i < fn.length; i++) {
+ fn[i]();
+ }
+}
+
+export function removeNodesWithUnmount(node: MutableNode, children: InulaNode[]) {
+ runLifeCycle(node.willUnmountFuncs);
+ removeNodes(node, children);
+ runLifeCycle(node.didUnmountFuncs);
+}
+
+export function geneNewNodesInEnvWithUnmount(node: MutableNode, newNodesFunc: () => VNode[]) {
+ startUnmountScope();
+ const nodes = geneNewNodesInCtx(node, newNodesFunc);
+ setUnmountFuncs(node);
+ return nodes;
+}
+export function getSavedCtxNodes(): Map> | null {
+ const contextNodeMap = getContextNodeMap();
+ if (contextNodeMap) {
+ return new Map([...contextNodeMap]);
+ }
+ return null;
+}
+/**
+ * @brief Initialize the new nodes, add parentEl to all nodes
+ */
+function initNewNodes(node: VNode, children: Array) {
+ // ---- Add parentEl to all children
+ addParentEl(children, node._$parentEl!);
+}
+/**
+ * @brief Generate new nodes in the saved context
+ * @param node
+ * @param newNodesFunc
+ * @returns
+ */
+
+export function geneNewNodesInCtx(node: MutableNode, newNodesFunc: () => Array) {
+ if (!node.savedContextNodes) {
+ // ---- No saved context, just generate new nodes
+ const newNodes = newNodesFunc();
+ // ---- Only for IfNode's same condition return
+ // ---- Initialize the new nodes
+ initNewNodes(node, newNodes);
+ return newNodes;
+ }
+ // ---- Save the current context nodes
+ const currentContextNodes = getContextNodeMap()!;
+ // ---- Replace the saved context nodes
+ replaceContext(node.savedContextNodes);
+ const newNodes = newNodesFunc();
+ // ---- Retrieve the current context nodes
+ replaceContext(currentContextNodes);
+ // ---- Only for IfNode's same condition return
+ // ---- Initialize the new nodes
+ initNewNodes(node, newNodes);
+ return newNodes;
+}
+
+/**
+ * @brief Remove nodes from parentEl and run willUnmount and didUnmount
+ */
+export function removeNodes(node: VNode, children: InulaNode[]) {
+ loopShallowEls(children, dom => {
+ node._$parentEl!.removeChild(dom);
+ });
+}
diff --git a/packages/inula-next/src/PropView.js b/packages/inula-next/src/PropView.js
deleted file mode 100644
index 2cb1701f..00000000
--- a/packages/inula-next/src/PropView.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import { DLNode } from './DLNode';
-import { insertNode } from './HTMLNode';
-export class PropView {
- propViewFunc;
- dlUpdateFunc = new Set();
-
- /**
- * @brief PropView constructor, accept a function that returns a list of DLNode
- * @param propViewFunc - A function that when called, collects and returns an array of DLNode instances
- */
- constructor(propViewFunc) {
- this.propViewFunc = propViewFunc;
- }
-
- /**
- * @brief Build the prop view by calling the propViewFunc and add every single instance of the returned DLNode to dlUpdateNodes
- * @returns An array of DLNode instances returned by propViewFunc
- */
- build() {
- let update;
- const addUpdate = updateFunc => {
- update = updateFunc;
- this.dlUpdateFunc.add(updateFunc);
- };
- const newNodes = this.propViewFunc(addUpdate);
- if (newNodes.length === 0) return [];
- if (update) {
- // Remove the updateNode from dlUpdateNodes when it unmounts
- DLNode.addWillUnmount(newNodes[0], this.dlUpdateFunc.delete.bind(this.dlUpdateFunc, update));
- }
-
- return newNodes;
- }
-
- /**
- * @brief Update every node in dlUpdateNodes
- * @param changed - A parameter indicating what changed to trigger the update
- */
- update(...args) {
- this.dlUpdateFunc.forEach(update => {
- update(...args);
- });
- }
-}
-
-export function insertChildren(el, propView) {
- insertNode(el, { _$nodes: propView.build(), _$dlNodeType: 7 }, 0);
-}
diff --git a/packages/inula-next/src/SnippetNode.js b/packages/inula-next/src/SnippetNode.js
deleted file mode 100644
index cb1522cf..00000000
--- a/packages/inula-next/src/SnippetNode.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { DLNode, DLNodeType } from './DLNode';
-import { cached } from './store';
-
-export class SnippetNode extends DLNode {
- constructor(depsArr) {
- super(DLNodeType.Snippet);
- this.depsArr = depsArr;
- }
-
- cached(deps, changed) {
- if (!deps || !deps.length) return false;
- const idx = Math.log2(changed);
- const prevDeps = this.depsArr[idx];
- if (cached(deps, prevDeps)) return true;
- this.depsArr[idx] = deps;
- return false;
- }
-}
diff --git a/packages/inula-next/src/TextNode.js b/packages/inula-next/src/TextNode.js
deleted file mode 100644
index 5a55bee9..00000000
--- a/packages/inula-next/src/TextNode.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { DLStore, cached } from './store';
-
-/**
- * @brief Shorten document.createTextNode
- * @param value
- * @returns Text
- */
-export function createTextNode(value, deps) {
- const node = DLStore.document.createTextNode(value);
- node.$$deps = deps;
- return node;
-}
-
-/**
- * @brief Update text node and check if the value is changed
- * @param node
- * @param value
- */
-export function updateText(node, valueFunc, deps) {
- if (cached(deps, node.$$deps)) return;
- const value = valueFunc();
- node.textContent = value;
- node.$$deps = deps;
-}
diff --git a/packages/inula-next/src/equal.ts b/packages/inula-next/src/equal.ts
new file mode 100644
index 00000000..d5a27fe1
--- /dev/null
+++ b/packages/inula-next/src/equal.ts
@@ -0,0 +1,10 @@
+/**
+ * @brief Shallowly compare the deps with the previous deps
+ * @param deps
+ * @param prevDeps
+ * @returns
+ */
+export function equal(deps: any[], prevDeps: any[]) {
+ if (!prevDeps || deps.length !== prevDeps.length) return false;
+ return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
+}
diff --git a/packages/inula-next/src/index.d.ts b/packages/inula-next/src/index.d.ts
deleted file mode 100644
index a3d3b4a0..00000000
--- a/packages/inula-next/src/index.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './types/index';
diff --git a/packages/inula-next/src/index.js b/packages/inula-next/src/index.js
deleted file mode 100644
index cef34797..00000000
--- a/packages/inula-next/src/index.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { DLNode } from './DLNode';
-import { insertNode } from './HTMLNode';
-
-export * from './HTMLNode';
-export * from './CompNode';
-export * from './EnvNode';
-export * from './TextNode';
-export * from './PropView';
-export * from './SnippetNode';
-export * from './MutableNode/ForNode';
-export * from './MutableNode/ExpNode';
-export * from './MutableNode/CondNode';
-export * from './MutableNode/TryNode';
-
-import { DLStore } from './store';
-
-export { setGlobal, setDocument } from './store';
-
-function initStore() {
- // Declare a global variable to store willUnmount functions
- DLStore.global.WillUnmountStore = [];
- // Declare a global variable to store didUnmount functions
- DLStore.global.DidUnmountStore = [];
-}
-
-/**
- * @brief Render the DL class to the element
- * @param {typeof import('./CompNode').CompNode} Comp
- * @param {HTMLElement | string} idOrEl
- */
-export function render(Comp, idOrEl) {
- let el = idOrEl;
- if (typeof idOrEl === 'string') {
- const elFound = DLStore.document.getElementById(idOrEl);
- if (elFound) el = elFound;
- else {
- throw new Error(`DLight: Element with id ${idOrEl} not found`);
- }
- }
- initStore();
- el.innerHTML = '';
- const dlNode = new Comp();
- dlNode._$init();
- insertNode(el, dlNode, 0);
- DLNode.runDidMount();
-}
-
-export function manual(callback, _deps) {
- return callback();
-}
-export function escape(arg) {
- return arg;
-}
-
-export const $ = escape;
-export const required = null;
-
-export function use() {
- console.error(
- 'DLight: use() is not supported be called directly. You can only assign `use(model)` to a dlight class property. Any other expressions are not allowed.'
- );
-}
diff --git a/packages/inula-next/src/index.ts b/packages/inula-next/src/index.ts
new file mode 100644
index 00000000..87ba1887
--- /dev/null
+++ b/packages/inula-next/src/index.ts
@@ -0,0 +1,199 @@
+import { runDidMount } from './lifecycle';
+import { insertNode } from './renderer/dom';
+import { equal } from './equal';
+import { constructComp, createCompNode, updateCompNode } from './CompNode';
+import { constructHook, createHookNode } from './HookNode';
+import { CompNode, VNode, InulaHTMLNode, HookNode, ChildrenNode } from './types';
+import { createContextNode, updateContextNode } from './ContextNode';
+import { InulaNodeType } from '@openinula/next-shared';
+import { createChildrenNode, updateChildrenNode } from './ChildrenNode';
+import { createForNode, updateForChildren, updateForNode } from './MutableNode/ForNode';
+import { createExpNode, updateExpNode } from './MutableNode/ExpNode';
+import { createCondNode, updateCondChildren, updateCondNode } from './MutableNode/CondNode';
+
+export * from './renderer/dom';
+export * from './CompNode';
+export * from './ContextNode';
+export * from './MutableNode/ForNode';
+export * from './MutableNode/ExpNode';
+export * from './MutableNode/CondNode';
+
+export type FunctionComponent = (props: Record) => CompNode | HookNode;
+
+export function render(compFn: FunctionComponent, container: HTMLElement): void {
+ if (container == null) {
+ throw new Error('Render target is empty. Please provide a valid DOM element.');
+ }
+ container.innerHTML = '';
+ const node = Comp(compFn);
+ insertNode(container as InulaHTMLNode, node, 0);
+ runDidMount();
+}
+
+// export function unmount(container: InulaHTMLNode): void {
+// const node = container.firstChild;
+// if (node) {
+// removeNode(node);
+// }
+// }
+
+export function untrack(callback: () => V): V {
+ return callback();
+}
+
+export let currentComp: CompNode | HookNode | null = null;
+
+export function inMount(): boolean {
+ return !!currentComp;
+}
+
+interface CompUpdater {
+ updateState: (bit: number) => void;
+ updateProp: (propName: string, newValue: unknown) => void;
+ getUpdateViews: () => [HTMLElement[], (bit: number) => HTMLElement[]];
+ updateDerived: (newValue: unknown, bit: number) => void;
+}
+
+export function Comp(compFn: FunctionComponent, props: Record = {}): CompNode {
+ return mountNode(() => createCompNode(), compFn, props);
+}
+
+function mountNode(
+ ctor: () => T,
+ compFn: FunctionComponent,
+ props: Record
+): T {
+ const compNode = ctor();
+ const prevNode = currentComp;
+ try {
+ currentComp = compNode;
+ compFn(props);
+ // eslint-disable-next-line no-useless-catch
+ } catch (err) {
+ throw err;
+ } finally {
+ currentComp = prevNode;
+ }
+ return compNode;
+}
+
+export function createComponent(compUpdater: CompUpdater): CompNode {
+ if (!currentComp || currentComp.__type !== InulaNodeType.Comp) {
+ throw new Error('Should not call createComponent outside the component function');
+ }
+ constructComp(currentComp, compUpdater);
+ return currentComp;
+}
+
+export function notCached(node: VNode, cacheId: string, cacheValues: unknown[]): boolean {
+ if (!cacheValues || !cacheValues.length) return false;
+ if (!node.$nonkeyedCache) {
+ node.$nonkeyedCache = {};
+ }
+ if (!equal(cacheValues, node.$nonkeyedCache[cacheId])) {
+ node.$nonkeyedCache[cacheId] = cacheValues;
+ return true;
+ }
+ return false;
+}
+
+export function didMount(fn: () => void) {
+ throw new Error('lifecycle should be compiled, check the babel plugin');
+}
+
+export function willUnmount(fn: () => void) {
+ throw new Error('lifecycle should be compiled, check the babel plugin');
+}
+
+export function didUnMount(fn: () => void) {
+ throw new Error('lifecycle should be compiled, check the babel plugin');
+}
+
+export function useHook(hookFn: (props: Record) => HookNode, params: unknown[], bitMap: number) {
+ if (currentComp) {
+ const props = params.reduce>((obj, val, idx) => ({ ...obj, [`p${idx}`]: val }), {});
+ // if there is the currentComp means we are mounting the component tree
+ return mountNode(() => createHookNode(currentComp!, bitMap), hookFn, props);
+ }
+ throw new Error('useHook must be called within a component');
+}
+
+export function createHook(compUpdater: CompUpdater): HookNode {
+ if (!currentComp || currentComp.__type !== InulaNodeType.Hook) {
+ throw new Error('Should not call createComponent outside the component function');
+ }
+ constructHook(currentComp, compUpdater);
+ return currentComp;
+}
+
+export function runOnce(fn: () => void): void {
+ if (currentComp) {
+ fn();
+ }
+}
+
+export function createNode(type: InulaNodeType, ...args: unknown[]): VNode | ChildrenNode {
+ switch (type) {
+ case InulaNodeType.Context:
+ return createContextNode(...(args as Parameters));
+ case InulaNodeType.Children:
+ return createChildrenNode(...(args as Parameters));
+ case InulaNodeType.Comp:
+ return createCompNode(...(args as Parameters));
+ case InulaNodeType.Hook:
+ return createHookNode(...(args as Parameters));
+ case InulaNodeType.For:
+ return createForNode(...(args as Parameters));
+ case InulaNodeType.Cond:
+ return createCondNode(...(args as Parameters));
+ case InulaNodeType.Exp:
+ return createExpNode(...(args as Parameters));
+ default:
+ throw new Error(`Unsupported node type: ${type}`);
+ }
+}
+
+export function updateNode(...args: unknown[]) {
+ const node = args[0] as VNode;
+ switch (node.__type) {
+ case InulaNodeType.Context:
+ updateContextNode(...(args as Parameters));
+ break;
+ case InulaNodeType.Children:
+ updateChildrenNode(...(args as Parameters));
+ break;
+ case InulaNodeType.For:
+ updateForNode(...(args as Parameters));
+ break;
+ case InulaNodeType.Cond:
+ updateCondNode(...(args as Parameters));
+ break;
+ case InulaNodeType.Exp:
+ updateExpNode(...(args as Parameters));
+ break;
+ case InulaNodeType.Comp:
+ case InulaNodeType.Hook:
+ updateCompNode(...(args as Parameters));
+ break;
+ default:
+ throw new Error(`Unsupported node type: ${node.__type}`);
+ }
+}
+
+export function updateChildren(...args: unknown[]) {
+ const node = args[0] as VNode;
+ switch (node.__type) {
+ case InulaNodeType.For:
+ updateForChildren(...(args as Parameters));
+ break;
+ case InulaNodeType.Cond:
+ updateCondChildren(...(args as Parameters));
+ break;
+ default:
+ throw new Error(`Unsupported node type: ${node.__type}`);
+ }
+}
+
+export { initContextChildren, createContext, useContext } from './ContextNode';
+export { initCompNode } from './CompNode';
+export { emitUpdate } from './HookNode';
diff --git a/packages/inula-next/src/lifecycle.ts b/packages/inula-next/src/lifecycle.ts
new file mode 100644
index 00000000..5be0ef89
--- /dev/null
+++ b/packages/inula-next/src/lifecycle.ts
@@ -0,0 +1,41 @@
+import { VNode, ScopedLifecycle } from './types';
+
+let DidMountStore: ScopedLifecycle;
+const WillUnmountStore: ScopedLifecycle[] = [];
+const DidUnmountStore: ScopedLifecycle[] = [];
+
+export const addWillUnmount = (node: VNode, func: (node: VNode) => void): void => {
+ const willUnmountStore = WillUnmountStore;
+ const currentStore = willUnmountStore[willUnmountStore.length - 1];
+ if (!currentStore) return;
+ currentStore.push(() => func(node));
+};
+
+export const addDidUnmount = (node: VNode, func: (node: VNode) => void): void => {
+ const didUnmountStore = DidUnmountStore;
+ const currentStore = didUnmountStore[didUnmountStore.length - 1];
+ if (!currentStore) return;
+ currentStore.push(() => func(node));
+};
+export const addDidMount = (node: VNode, func: (node: VNode) => void): void => {
+ if (!DidMountStore) DidMountStore = [];
+ DidMountStore.push(() => func(node));
+};
+
+export const runDidMount = (): void => {
+ const didMountStore = DidMountStore;
+ if (!didMountStore || didMountStore.length === 0) return;
+ for (let i = didMountStore.length - 1; i >= 0; i--) {
+ didMountStore[i]();
+ }
+ DidMountStore = [];
+};
+
+export function startUnmountScope() {
+ WillUnmountStore.push([]);
+ DidUnmountStore.push([]);
+}
+
+export function endUnmountScope() {
+ return [WillUnmountStore.pop(), DidUnmountStore.pop()];
+}
diff --git a/packages/inula-next/src/renderer/dom.ts b/packages/inula-next/src/renderer/dom.ts
new file mode 100644
index 00000000..39803d6f
--- /dev/null
+++ b/packages/inula-next/src/renderer/dom.ts
@@ -0,0 +1,222 @@
+import { getFlowIndexFromNodes, appendNodesWithIndex, addParentEl } from '../InulaNode';
+import { equal } from '../equal';
+import { InulaHTMLNode, TextNode, InulaNode } from '../types';
+
+const delegatedEvents = new Set();
+
+/**
+ * @brief Shortcut for document.createElement
+ * @param tag
+ * @returns HTMLElement
+ */
+export function createElement(tag: string): HTMLElement {
+ return document.createElement(tag);
+}
+
+/**
+ * @brief Shorten document.createTextNode
+ * @param value
+ * @returns Text
+ */
+export function createTextNode(value: string, deps?: unknown[]) {
+ const node = document.createTextNode(value) as unknown as TextNode;
+ if (deps) node.deps = deps;
+ return node;
+}
+
+/**
+ * @brief Update text node and check if the value is changed
+ * @param node
+ * @param value
+ */
+export function updateText(node: TextNode, valueFunc: () => string, deps: unknown[]) {
+ if (equal(deps, node.deps)) return;
+ const value = valueFunc();
+ node.textContent = value;
+ node.deps = deps;
+}
+
+function cache(el: HTMLElement, key: string, deps: any[]): boolean {
+ if (deps.length === 0) return false;
+ const cacheKey = `$${key}`;
+ if (equal(deps, (el as any)[cacheKey])) return true;
+ (el as any)[cacheKey] = deps;
+ return false;
+}
+
+const isCustomProperty = (name: string): boolean => name.startsWith('--');
+
+interface StyleObject {
+ [key: string]: string | number | null | undefined;
+}
+
+export function setStyle(el: InulaHTMLNode, newStyle: CSSStyleDeclaration): void {
+ const style = el.style;
+ const prevStyle = el._prevStyle || {};
+
+ // Remove styles that are no longer present
+ for (const key in prevStyle) {
+ // eslint-disable-next-line no-prototype-builtins
+ if (prevStyle.hasOwnProperty(key) && (newStyle == null || !newStyle.hasOwnProperty(key))) {
+ if (isCustomProperty(key)) {
+ style.removeProperty(key);
+ } else if (key === 'float') {
+ style.cssFloat = '';
+ } else {
+ style[key] = '';
+ }
+ }
+ }
+
+ // Set new or changed styles
+ for (const key in newStyle) {
+ const prevValue = prevStyle[key];
+ const newValue = newStyle[key];
+ // eslint-disable-next-line no-prototype-builtins
+ if (newStyle.hasOwnProperty(key) && newValue !== prevValue) {
+ if (newValue == null || newValue === '' || typeof newValue === 'boolean') {
+ if (isCustomProperty(key)) {
+ style.removeProperty(key);
+ } else if (key === 'float') {
+ style.cssFloat = '';
+ } else {
+ style[key] = '';
+ }
+ } else if (isCustomProperty(key)) {
+ style.setProperty(key, newValue);
+ } else if (key === 'float') {
+ style.cssFloat = newValue;
+ } else {
+ el.style[key] = newValue;
+ }
+ }
+ }
+
+ // Store the new style for future comparisons
+ el._prevStyle = { ...newStyle };
+}
+
+/**
+ * @brief Plainly set dataset
+ * @param el
+ * @param value
+ */
+
+export function setDataset(el: HTMLElement, value: { [key: string]: string }): void {
+ Object.assign(el.dataset, value);
+}
+
+/**
+ * @brief Set HTML property with checking value equality first
+ */
+
+export function setHTMLProp(el: HTMLElement, key: string, valueFunc: () => any, deps: any[]): void {
+ // ---- Comparing deps, same value won't trigger
+ // will lead to a bug if the value is set outside of the DLNode
+ // e.g. setHTMLProp(el, "textContent", "value", [])
+ // => el.textContent = "other"
+ // => setHTMLProp(el, "textContent", "value", [])
+ // The value will be set to "other" instead of "value"
+ if (cache(el, key, deps)) return;
+ (el as any)[key] = valueFunc();
+}
+
+/**
+ * @brief Plainly set HTML properties
+ */
+
+export function setHTMLProps(el: InulaHTMLNode, value: HTMLAttrsObject): void {
+ Object.entries(value).forEach(([key, v]) => {
+ if (key === 'style') return setStyle(el, v as CSSStyleDeclaration);
+ if (key === 'dataset') return setDataset(el, v as { [key: string]: string });
+ setHTMLProp(el, key, () => v, []);
+ });
+}
+
+/**
+ * @brief Set HTML attribute with checking value equality first
+ * @param el
+ * @param key
+ * @param valueFunc
+ * @param deps
+ */
+
+export function setHTMLAttr(el: InulaHTMLNode, key: string, valueFunc: () => string, deps: any[]): void {
+ if (cache(el, key, deps)) return;
+ el.setAttribute(key, valueFunc());
+}
+
+interface HTMLAttrsObject {
+ [key: string]: string | number | boolean | object | undefined;
+ style?: CSSStyleDeclaration;
+ dataset?: { [key: string]: string };
+}
+
+/**
+ * @brief Plainly set HTML attributes
+ * @param el
+ * @param value
+ */
+
+export function setHTMLAttrs(el: InulaHTMLNode, value: HTMLAttrsObject): void {
+ Object.entries(value).forEach(([key, v]) => {
+ setHTMLAttr(el, key, () => v, []);
+ });
+}
+
+/**
+ * @brief Set memorized event, store the previous event in el[`$on${key}`], if it exists, remove it first
+ * @param el
+ * @param key
+ * @param value
+ */
+
+export function setEvent(el: InulaHTMLNode, key: string, value: EventListener): void {
+ const prevEvent = el[`$on${key}`];
+ if (prevEvent) el.removeEventListener(key, prevEvent);
+ el.addEventListener(key, value);
+ el[`$on${key}`] = value;
+}
+
+function eventHandler(e: Event): void {
+ const key = `$$${e.type}`;
+ for (const node of e.composedPath()) {
+ if (node[key]) node[key](e);
+ if (e.cancelBubble) return;
+ }
+}
+
+export function delegateEvent(el: InulaHTMLNode, key: string, value: EventListener): void {
+ if (el[`$$${key}`] === value) return;
+ el[`$$${key}`] = value;
+ if (!delegatedEvents.has(key)) {
+ delegatedEvents.add(key);
+ document.addEventListener(key, eventHandler);
+ }
+}
+
+export function appendNode(el: InulaHTMLNode, child: InulaHTMLNode) {
+ // ---- Set _$nodes
+ if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[];
+ el._$nodes.push(child);
+
+ el.appendChild(child);
+}
+
+/**
+ * @brief Insert any DLNode into an element, set the _$nodes and append the element to the element's children
+ * @param el
+ * @param node
+ * @param position
+ */
+export function insertNode(el: InulaHTMLNode, node: InulaNode, position: number): void {
+ // ---- Set _$nodes
+ if (!el._$nodes) el._$nodes = Array.from(el.childNodes) as InulaHTMLNode[];
+ el._$nodes.splice(position, 0, node);
+
+ // ---- Insert nodes' elements
+ const flowIdx = getFlowIndexFromNodes(el._$nodes, node);
+ appendNodesWithIndex([node], el, flowIdx);
+ // ---- Set parentEl
+ addParentEl([node], el);
+}
diff --git a/packages/inula-next/src/scheduler.js b/packages/inula-next/src/scheduler.ts
similarity index 90%
rename from packages/inula-next/src/scheduler.js
rename to packages/inula-next/src/scheduler.ts
index 2b454b4c..73d900ca 100644
--- a/packages/inula-next/src/scheduler.js
+++ b/packages/inula-next/src/scheduler.ts
@@ -1,25 +1,23 @@
-/*
- * 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.
- */
-
-const p = Promise.resolve();
-
-/**
- * Schedule a task to run in the next microtask.
- *
- * @param {() => void} task
- */
-export function schedule(task) {
- p.then(task);
-}
+/*
+ * 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.
+ */
+
+const p = Promise.resolve();
+
+/**
+ * Schedule a task to run in the next microtask.
+ */
+export function schedule(task: () => void) {
+ p.then(task);
+}
diff --git a/packages/inula-next/src/store.js b/packages/inula-next/src/store.js
deleted file mode 100644
index 199a9899..00000000
--- a/packages/inula-next/src/store.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Store } from '@openinula/store';
-
-// ---- Using external Store to store global and document
-// Because Store is a singleton, it is safe to use it as a global variable
-// If created in DLight package, different package versions will introduce
-// multiple Store instances.
-
-if (!('global' in Store)) {
- if (typeof window !== 'undefined') {
- Store.global = window;
- } else if (typeof global !== 'undefined') {
- Store.global = global;
- } else {
- Store.global = {};
- }
-}
-if (!('document' in Store)) {
- if (typeof document !== 'undefined') {
- Store.document = document;
- }
-}
-
-export const DLStore = { ...Store, delegatedEvents: new Set() };
-
-export function setGlobal(globalObj) {
- DLStore.global = globalObj;
-}
-
-export function setDocument(customDocument) {
- DLStore.document = customDocument;
-}
-
-/**
- * @brief Compare the deps with the previous deps
- * @param deps
- * @param prevDeps
- * @returns
- */
-export function cached(deps, prevDeps) {
- if (!prevDeps || deps.length !== prevDeps.length) return false;
- return deps.every((dep, i) => !(dep instanceof Object) && prevDeps[i] === dep);
-}
diff --git a/packages/inula-next/src/types.ts b/packages/inula-next/src/types.ts
new file mode 100644
index 00000000..da44a766
--- /dev/null
+++ b/packages/inula-next/src/types.ts
@@ -0,0 +1,145 @@
+/*
+ * 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 { InulaNodeType } from '@openinula/next-shared';
+
+export type Lifecycle = () => void;
+export type ScopedLifecycle = Lifecycle[];
+
+export { type Properties as CSSProperties } from 'csstype';
+
+export type InulaNode = VNode | TextNode | InulaHTMLNode;
+
+export interface VNode {
+ __type: InulaNodeType;
+ _$nodes: InulaNode[];
+ _$parentEl?: InulaHTMLNode;
+ $nonkeyedCache?: Record;
+}
+
+export interface TextNode extends Text {
+ deps: unknown[];
+}
+
+export interface InulaHTMLNode extends HTMLElement {
+ _$nodes: InulaNode[];
+ _prevStyle?: CSSStyleDeclaration;
+ [key: `$on${string}`]: EventListener
+ [key: `$$${string}`]: EventListener
+}
+
+export interface ComposableNode = Record> extends VNode {
+ __type: InulaNodeType;
+ parent?: ComposableNode;
+ props: Props;
+ cache?: Record;
+ _$nodes: InulaHTMLNode[];
+ mounting?: boolean;
+ _$unmounted?: boolean;
+ _$forwardPropsSet?: Set;
+ _$forwardPropsId?: string[];
+ _$contentKey?: string;
+ _$depNumsToUpdate?: number[];
+ updateState: (bit: number) => void;
+ updateProp: (...args: any[]) => void;
+ updateContext?: (context: any, key: string, value: any) => void;
+ getUpdateViews?: () => any;
+ didUnmount?: () => void;
+ willUnmount?: () => void;
+ didMount?: () => void;
+ updateView?: (depNum: number) => void;
+}
+
+export interface CompNode extends ComposableNode {
+ __type: InulaNodeType.Comp;
+}
+
+export interface HookNode extends ComposableNode {
+ __type: InulaNodeType.Hook;
+ bitmap: number;
+ parent: HookNode | CompNode;
+ value?: () => unknown;
+}
+
+/**
+ * @brief Mutable node is a node that this._$nodes can be changed, things need to pay attention:
+ * 1. The context of the new nodes should be the same as the old nodes
+ * 2. The new nodes should be added to the parentEl
+ * 3. The old nodes should be removed from the parentEl
+ */
+export interface MutableNode extends VNode {
+ willUnmountFuncs: UnmountShape;
+ didUnmountFuncs: UnmountShape;
+ savedContextNodes: Map> | null;
+}
+
+export interface CondNode extends MutableNode {
+ cond: number;
+ didntChange: boolean;
+ __type: InulaNodeType.Cond;
+ depNum: number;
+ condFunc: (condNode: CondNode) => VNode[];
+ /**
+ * @brief assigned by condNode.condFunc in compile time
+ * @param changed
+ * @returns
+ */
+ updateFunc?: (changed: number) => void;
+ _$parentEl?: InulaHTMLNode;
+}
+
+export interface ExpNode extends MutableNode {
+ __type: InulaNodeType.Exp;
+ _$nodes: InulaNode[];
+ _$parentEl?: InulaHTMLNode;
+ deps: unknown[];
+}
+
+export interface ForNode extends MutableNode