diff --git a/api-2.1-fn.md b/api-2.1-fn.md new file mode 100644 index 00000000..8e584688 --- /dev/null +++ b/api-2.1-fn.md @@ -0,0 +1,55 @@ +```js +function MyComp({prop1}) { + let count = 1 + let doubleCount = count * 2 + + const updateCount = () => { + count++ + } + + return
{prop1}
; +} +``` + +```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) => +
{item}
+ }
+ ) +} +``` + + +## functional syntax level support +### 1. React inherited flavor: Expression mapping +```jsx +function MyComp() { + let arr = [1, 2, 3] + return ( + <> + {arr.map(item =>
{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 =>

{item}

) + .map(item =>
{item}
) + } + + ) +} +``` +will be converted into: +```jsx +function MyComp() { + let arr = [1, 2, 3] + return ( +

{item}

) + }>{(item) => +
{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
{count}
+ } + function InternalComp({ doubleCount }) { + return ( +
+ {count /* This is Parent's state */} + {doubleCount /* This is current's prop */} +
+ ) + } + + return ( +
+ {jsxSlice} + {jsxArray} + {jsxFunc()} + +
+ ) +} +``` +first, we convert the JSX into nested components: +```jsx +function MyComp() { + let count = 100 + function Comp_$id1$() { + return
{count}
+ } + const jsxSlice = + function Comp_$id2$() { + return
1
+ } + function Comp_$id3$() { + return
{count}
+ } + 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
cc is 200
+ } + function Comp_$id5$() { + return
cc is 100
+ } + function Comp_$id6$() { + return
Flag is true
+ } + return ( + <> + + + + + + + + + + + ) + } + function Comp_$id7$() { + return
Not 100 {flag}
+ } + return ( + <> + + + + + + + + ) + } + return ( + <> + + + + + + + + ) +} +``` +Same rule will be applied to switch statements(TBD). + +## How to pass view props to another component +Consider the following 3 types of solutions: +1. A calculated jsx slice: +```jsx +function MyComp1() { + let count = 0 + return {count}}/> +} +function Comp({ view }) { + return <>{view} +} +``` +will be converted into: +```jsx +class MyComp1 extends View { + count = 0 + @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) + } + } + this._$updates.add(update) + return node0 + } + } + })() + Body() { + const node0 = new Comp({ + view: new this.Comp_$id$() + }) + + return node0 + } +} + +class Comp extends View { + @Prop view + + Body() { + const node0 = new Fragment() + const node1 = new ExpressionNode(this.view) + appendChild(node0, [node1]) + const update = changed => { + if (changed & 0x0001) { + node1.update(this.view) + } + } + this._$updates.add(update) + return node0 + } +} +``` +In this case, the `view` prop is a calculated JSX slice, but updating the `count` state won't re-render the whole view, only the `count` part inside the `Comp_$id$` component. So feel free to use it! + +2. Functional nested Component: +```jsx +function MyComp2() { + let count = 0 + function SubComp() { + return
{count}
+ } + return +} +function Comp({ View }) { + return +} +``` +will be converted into: +```jsx +class MyComp2 extends View { + count = 0 + @Static SubComp = (() => { + 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 + } + } + })() + Body() { + const node0 = new Comp({ + View: this.SubComp + }) + return node0 + } +} +class Comp extends View { + @Prop View + + Body() { + const node0 = new this.View() + return node0 + } +} +``` +This is a good way to pass props to another component if you want to add some props in the `Comp`. + +3. Functional calculated JSX slice: +```jsx +function MyComp3() { + let count = 0 + return
{count}
}/> +} +function Comp({ viewFunc }) { + return <>{viewFunc()} +} +``` +will be converted into: +```jsx +class MyComp3 extends View { + count = 0 + Body() { + const node0 = new Fragment() + const node1 = new Comp({ + viewFunc: () => { + const 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) + } + } + this._$updates.add(update) + return node0 + } + } + })() + return new Comp_$id$() + } + }) + appendChild(node0, [node1]) + const update = changed => { + if (changed & 0x0001) { + node1.updateProp("viewFunc", () => { + const Comp_$id$ = (() => { + const _$this0 = this + return class extends View { + willUnmount() { + 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 + } + } + })() + return new Comp_$id$() + }) + } + } + this._$updates.add(update) + return node0 + } +} + +``` +All the view will be re-calculated when the `count` state changes. So Don't Ever Use This! + +# Statements +## 1. Assignment will be converted to class properties as stated above +## 2. If statements will be treated as part of the early return logic +## 3. All other statements will be converted as part of the willMount lifecycle +```jsx +function MyComp() { + let count = 100 + console.log(count) + console.log("Hello") + anyFunction() + ... +} +``` +will be converted as: +```jsx +class MyComp extends View { + count = 100 + + willMount() { + console.log(this.count) + console.log("Hello") + anyFunction() + } +} +``` + +# Lifecycle +## 1. willMount +```jsx +function MyComp() { + let count = 100 + console.log("willMount1") + willMount(() => { + const a = 1 + console.log("willMount2") + }) + willMount(() => { + const a = 1 + console.log("willMount3") + }) +} +``` +will be converted as: +```jsx +class MyComp extends View { + count = 100 + + willMount() { + console.log("willMount1") + // transform into iife because of its internal variables + (() => { + const a = 1 + console.log("willMount2") + })() + (() => { + const a = 1 + console.log("willMount3") + })() + } +} +``` + +## 2. didMount/willUnmount/didUnmount +```jsx +function MyComp() { + didMount(() => { + console.log("didMount") + }) + willUnmount(async () => { + console.log("willUnmount") + }) + didUnmount(() => { + console.log("didUnmount") + }) +} +``` +will be converted as: +```jsx +class MyComp extends View { + didMount() { + (() => { + console.log("didMount") + })() + } + willUnmount() { + (async () => { + console.log("willUnmount") + })() + } + didUnmount() { + (() => { + console.log("didUnmount") + })() + } +} +``` + +# Watch +```jsx +function MyComp() { + let count = 100 + watch(() => { + console.log(count) + }) +} +``` +will be converted as: +```jsx +class MyComp extends View { + count = 100 + + @Watch + _$watch_$id$() { + console.log(this.count) + } +} +``` +With manual dependencies: +```jsx +function MyComp() { + let count = 100 + let doubleCount = count * 2 + watch(() => { + console.log(count, doubleCount) + }, [count, doubleCount]) +} +``` +will be converted as: +```jsx +class MyComp extends View { + count = 100 + doubleCount = this.count * 2 + @Watch("count", "doubleCount") + _$watch_$id$() { + console.log(this.count) + } +} +``` +NOTE: convert `identifier` to `string literal` in the `@Watch` decorator + + + +# Hook +Same logic as the Component +```jsx +function useMyHook() { + let count = 100 + + return count +} +``` +will be converted into: +```jsx +class MyHook extends Model { + count = 100 + + _$return = this.count +} +``` + +used in a component: +```jsx +function MyComp() { + const count = useMyHook() + return
{count}
+} +``` +will be converted into: +```jsx +class MyComp extends View { + count = use(useMyHook)._$return + + Body() { + toBeCompiled(
{this.count}
) + } +} +``` +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
{count} {doubleCount}
+} +``` +will be converted into: +```jsx +class MyComp extends View { + count + doubleCount + @Watch + _$deconstruct_$id$() { + const { count, doubleCount } = use(useMyHook)._$return + this.count = count + this.doubleCount = doubleCount + } + + Body() { + toBeCompiled(
{this.count} {this.doubleCount}
) + } +} +``` + +Early return in hooks: +```jsx +function useMyHook() { + let count = 100 + if (count === 100) { + return 100 + } + let flag = count === 200 + return flag +} +``` +will be converted first into: +```jsx +function useMyHook() { + let count = 100 + function Hook_$id1$() { + return count + } + function Hook_$id2$() { + let flag = count === 200 + return flag + } + + if (count === 100) { + return Hook_$id1$() + } else { + return Hook_$id2$() + } +} +``` +and then into: +```jsx +class useMyHook extends Model { + count = 100 + + Hook_$id1$ = (() => { + const _$this0 = this + return class { + _$return = _$this0.count + } + })() + + Hook_$id2$ = (() => { + const _$this0 = this + return class { + _$return = _$this0.count === 200 + } + })() + + _$return = (() => { + let ifIdx, model + if (this.count === 100) { + if (ifIdx !== 0) { + ifIdx = 0 + model = use(this.Hook_$id1$)._$return + } + } else { + if (ifIdx !== 1) { + ifIdx = 1 + model = use(this.Hook_$id2$)._$return + } + } + + return model + })() +} +``` + + + +# Compilers +Starter Example: +```jsx +function MyComp(props) { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + const jsxSlice =
{count}
+ + const jsxFunc = () => { + return
{count}
+ } + + function InnerComp() { + let newCount = 100 + return
{newCount}
+ } + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + if (count === 100) { + return
100
+ } + + let flag = true + + return ( +
+ {flag} +

{count}

+ {(item) => { + const {id, data} = item + return
{data}
+ }}
+ {data.map(item =>
{item}
)} + {jsxSlice} + +
+ ) +} +``` + +## 1. Auto Naming Compiler +```jsx +const MyComp = Component(props => { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + const jsxSlice =
{count}
+ + const jsxFunc = () => { + return
{count}
+ } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + if (count === 100) { + return
100
+ } + + let flag = true + + return ( +
+ {flag} +

{count}

+ {(item, idx) => { + const {id, data} = item + return
{data}{idx}
+ }}
+ {data.map(item =>
{item}
)} + {jsxSlice} + +
+ ) +}) +``` + +## 2. JSX Slice Compiler +Identify all non-returning JSX expressions +```jsx +const MyComp = Component(props => { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + + const jsxSlice = new (Component(() => { + return
{count}
+ }))() + + const jsxFunc = () => { + return new (Component(() => { + return
{count}
+ }))() + } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + if (count === 100) { + return
100
+ } + + let flag = true + + return ( +
+ {flag} +

{count}

+ {(item, idx) => { + const {id, data} = item + return
{data}{idx}
+ }}
+ {data.map(item =>
{item}
)} + {jsxSlice} + +
+ ) +}) +``` + +## 3. For Loop Compiler - mapping to for tag +```jsx +const MyComp = Component(props => { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + + const jsxSlice = new (Component(() => { + return
{count}
+ }))() + + const jsxFunc = () => { + return new (Component(() => { + return
{count}
+ }))() + } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + if (count === 100) { + return
100
+ } + + let flag = true + + return ( +
+ {flag} +

{count}

+ {(item, idx) => { + const {id, data} = item + return
{data}{idx}
+ }}
+ {(item) => ( +
{item}
+ )}
+ {jsxSlice} + +
+ ) +}) +``` + +## 4. For Loop Compiler - sub component extraction +```jsx +const MyComp = Component(props => { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + + const jsxSlice = new (Component(() => { + return
{count}
+ }))() + + const jsxFunc = () => { + return new (Component(() => { + return
{count}
+ }))() + } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + if (count === 100) { + return
100
+ } + + const Comp_je104nt = Component(({ item, idx }) => { + const {id, data} = item + return
{data}{idx}
+ }) + + return ( +
+

{count}

+ {(item, idx) => ( + + )} + {(item) => ( +
{item}
+ )}
+ {jsxSlice} + +
+ ) +}) +``` + +## 5. Early return Compiler +```jsx +const MyComp = Component(props => { + let { prop1, prop2: [p20, p21], ...otherProps } = props + let count = 100 + let data = props.data + + const jsxSlice = new (Component(() => { + return
{count}
+ }))() + + const jsxFunc = () => { + return new (Component(() => { + return
{count}
+ }))() + } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + const Comp_je104nt = Component(({ item, idx }) => { + const {id, data} = item + return
{data}{idx}
+ }) + + const Comp_jf91a2 = Component(() => { + return
100
+ }) + + const Comp_ao528j = Component(() => { + let flag = true + return ( +
+ {flag} +

{count}

+ {(item, idx) => ( + + )} + {(item) => ( +
{item}
+ )}
+ {jsxSlice} + +
+ ) + }) + + return ( + + + + + + + ) +}) +``` + +---- +Till now, all the implicit components are created, we can now recursively convert them into classes. +---- + +## 6. Props Compiler +```jsx +const MyComp = Component({ prop1, prop2, data, ...otherProps } => { + let p20, p21 + watch(() => { + [p20, p21] = prop2 + }) + let count = 100 + + // let data = data -> automatically deleted + const jsxSlice = new (Component(() => { + return
{count}
+ }))() + + const jsxFunc = () => { + return new (Component(() => { + return
{count}
+ }))() + } + + const InnerComp = Component(() => { + let newCount = 100 + return
{newCount}
+ }) + + console.log("hello") + didMount(() => { + console.log("didMount") + }) + + const Comp_je104nt = Component(({ item, idx }) => { + let id, data + watch(() => { + {id, data} = item + }) + return
{data}{idx}
+ }) + + const Comp_jf91a2 = Component(() => { + return
100
+ }) + + const Comp_ao528j = Component(() => { + let flag = true + return ( +
+ {flag} +

{count}

+ {(item, idx) => ( + + )} + {(item) => ( +
{item}
+ )}
+ {jsxSlice} + +
+ ) + }) + + return ( + + + + + + + ) +}) +``` + + +## 7. Class converter +`jsxSlice` and other inner components will be tagged as `InnerComponents`, with extra `_$this` scope: +```jsx +const MyComp = class extends View { + @Prop prop1 + @Prop prop2 + @Prop data + @RestProps otherProps + + p20 + p21 + @Watch + _$watch_aei1nf$() { + [p20, p21] = this.prop2 + } + + count = 100 + + jsxSlice = new class extends View { + Body() { +
{count}
+ } + }() + + jsxFunc = () => { + return new class extends View { + Body() { +
{count}
+ } + }() + } + + InnerComp = class extends View { + newCount = 100 + Body() { +
{newCount}
+ } + } + + Comp_je104nt = class extends View { + @Prop item + @Prop idx + id + data + @Watch + _$watch_15oae3$() { + {id, data} = this.item + } + + Body() { +
{data}{idx}
+ } + } + + Comp_jf91a2 = class extends View { + Body() { +
100
+ } + } + + Comp_ao528j = class extends View { + flag = true + Body() { +
+ {flag} +

{count}

+ {(item, idx) => ( + + )} + {(item) => ( +
{item}
+ )}
+ {jsxSlice} + +
+ } + } + + willMount() { + console.log("hello") + } + didMount() { + (() => { + console.log("didMount") + })() + } + + + Body() { + + + + + + + } +} +``` + +## 8. This converter +Auto add `this` to variables also record the `_$thisX` scope to auto add: +```jsx +const MyComp = class extends View { + @Prop prop1 + @Prop prop2 + @Prop data + @RestProps otherProps + + p20 + p21 + @Watch + _$watch_aei1nf$() { + [this.p20, this.p21] = this.prop2 + } + + count = 100 + + jsxSlice = new ((() => { + const _$this0 = this + return class extends View { + Body() { +
{_$this0.count}
+ } + } + })())() + + jsxFunc = () => { + return new ((() => { + const _$this0 = this + return class extends View { + Body() { +
{_$this0.count}
+ } + } + })())() + } + + InnerComp = class extends View { + newCount = 100 + Body() { +
{this.newCount}
+ } + } + + Comp_je104nt = class extends View { + @Prop item + @Prop idx + id + data + @Watch + _$watch_15oae3$() { + {this.id, this.data} = this.item + } + + Body() { +
{this.data}{this.idx}
+ } + } + + Comp_jf91a2 = class extends View { + Body() { +
100
+ } + } + + Comp_ao528j = (() => { + const _$this0 = this + return class extends View { + flag = true + Body() { +
+ {this.flag} +

{_$this0.count}

+ {(item, idx) => ( + <_$this0.Comp_$id1$ item={item} idx={idx}/> + )} + {(item) => ( +
{item}
+ )}
+ {_$this0.jsxSlice} + <_$this0.InnerComp /> +
+ } + } + })() + + willMount() { + console.log("hello") + } + didMount() { + (() => { + console.log("didMount") + })() + } + + + Body() { + + + + + + + } +} +``` + +## 8. Decorator Compiler + + +## 9. JSX Compiler +InnerComponent need to +1. add a `willUnmount` lifecycle to clear the parent updates. +2. push the `update` function to all the parent's updates. + +```jsx + InnerComp = class extends View { + newCount = 100 + Body() { +
{this.newCount}
+ } + } +``` +To +```jsx + InnerComp = class extends View { + newCount = 100 + willUnmount() { + clear(this) + } + Body() { + const node0 = createElement("div") + const node1 = new ExpressionNode(this.newCount) + appendChild(node0, [node1]) + const update = changed => { + if (changed & 0x0001) { + node1.update(this.newCount) + } + } + this._$updates.add(update) + return node0 + } + } +``` + + + +# function vs class +```jsx +function MyComp() { + let count = 100 + return +} +``` +to +```jsx +class MyComp extends View { + count = 100 + Body() { + const node0 = new MyComp2({ + count: this.count + }) + return node0 + } +} +``` + +vs +```jsx +function MyComp(props) { + return new class extends View { + @Prop count + Body() { + const node0 = MyComp2({ + count: this.count + }) + return node0 + } + }(props) +} + +MyComp({ count: 100 }) +// or + +``` + + +```jsx +// declare +class Comp extends View {} +// use +new Comp(props, children) + +// declare +function Comp(props, children) { + return new class extends View {}(props, children) +} +// use +Comp(props, children) +``` + + +```jsx +function MyComp({ defaultCount }) { + let count = defaultCount + return
{count}
+} + +function App() { + return +} + +function App2() { + const comp = MyComp({defaultCount: 200}) + + console.log(comp) + return <> + {MyComp({defaultCount: 200})} + +} + +``` +```js +class MyComp { + @Prop defaultCount + count + Body() { +
{this.count}
+ } +} +class App { + Body() { + const node0 = new MyComp({ + defaultCount: 200 + }) + } +} +``` +```js +class MyCompInternalaefaefaefea { + @Prop defaultCount + count + Body() { +
{this.count}
+ } + } +function Factory(cls) { + return ...args => new cls(...args) +} +const MyComp = Factory(MyCompInternalaefaefaefea) + +function App() { + class AppInternal { + Body() { + const node0 = MyComp() + } + } + + return new AppInternal() +} +function App2() { + return <> + {MyComp({defaultCount: 200})} + +} +function App() { + class AppInternal { + Body() { + const node0 = new Fragment() + const node1 = new ExpressionNode( + MyComp({defaultCount: 200}) + ) + appendChild(node0, [node1]) + + return node0 + } + } + + return new AppInternal() +} +``` + + + + + + + + + + + +to +```jsx +function MyComp({ defaultCount }) { + let count = defaultCount + return div(null, [count]) +} + +function MyComp2() { + return MyComp({ defaultCount: 200 }) +} +``` diff --git a/demos/benchmark/package.json b/demos/benchmark/package.json index cfc37a65..55ee2f55 100644 --- a/demos/benchmark/package.json +++ b/demos/benchmark/package.json @@ -12,14 +12,14 @@ "@babel/standalone": "^7.22.4", "@openinula/next": "workspace:*", "@iandx/easy-css": "^0.10.14", - "babel-preset-inula-next": "workspace:*" + "@openinula/babel-preset-inula-next": "workspace:*" }, "devDependencies": { "typescript": "^5.2.2", "vite": "^4.4.9", - "vite-plugin-inula-next": "workspace:*" + "@openinula/vite-plugin-inula-next": "workspace:*" }, "keywords": [ - "dlight.js" + "inula-next" ] } diff --git a/demos/benchmark/src/main.jsx b/demos/benchmark/src/main.jsx index ddaa9bf6..540fba2d 100644 --- a/demos/benchmark/src/main.jsx +++ b/demos/benchmark/src/main.jsx @@ -74,31 +74,39 @@ function Button({ id, text, fn }) { function App() { let data = []; let selected = null; + function run() { data = buildData(1000); } + function runLots() { data = buildData(10000); } + function add() { data.push(...buildData(1000)); } + function update() { for (let i = 0; i < data.length; i += 10) { data[i].label += ' !!!'; } } + function swapRows() { if (data.length > 998) { [data[1], data[998]] = [data[998], data[1]]; } } + function clear() { data = []; } + function remove(id) { data = data.filter(d => d.id !== id); } + function select(id) { selected = id; } @@ -124,19 +132,21 @@ function App() { - - - - - + + {({ id, label }) => ( + + + + + )}
- - - - - - -
+ + + + + + +
diff --git a/demos/benchmark/vite.config.js b/demos/benchmark/vite.config.js index 0531be34..92e90858 100644 --- a/demos/benchmark/vite.config.js +++ b/demos/benchmark/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from 'vite'; -import inula from 'vite-plugin-inula-next'; +import inula from '@openinula/vite-plugin-inula-next'; export default defineConfig({ server: { diff --git a/demos/todo-mvc/app.css b/demos/todo-mvc/app.css new file mode 100644 index 00000000..de87dd40 --- /dev/null +++ b/demos/todo-mvc/app.css @@ -0,0 +1,417 @@ +@charset "utf-8"; + +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: '❯'; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #C18585; +} + +.todo-list li .destroy:after { + content: '×'; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #DB7676; +} + +.filters li a.selected { + border-color: #CE4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +:focus, +.toggle:focus + label, +.toggle-all:focus + label { + box-shadow: 0 0 2px 2px #CF7D7D; + outline: 0; +} + + +.visually-hidden { + border: 0; + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + white-space: nowrap; +} + +.toggle-all { + width: 40px !important; + height: 60px !important; + right: auto !important; +} + +.toggle-all-label { + pointer-events: none; +} diff --git a/demos/todo-mvc/app.jsx b/demos/todo-mvc/app.jsx new file mode 100644 index 00000000..272de4f1 --- /dev/null +++ b/demos/todo-mvc/app.jsx @@ -0,0 +1,90 @@ +/* + * 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'; +import { Header } from './components/header'; +import { Main } from './components/main'; +import { + ADD_ITEM, + UPDATE_ITEM, + REMOVE_ITEM, + TOGGLE_ITEM, + REMOVE_ALL_ITEMS, + TOGGLE_ALL, + REMOVE_COMPLETED_ITEMS, +} from './constants'; +import './app.css'; +import { Footer } from './components/footer.jsx'; +// This alphabet uses `A-Za-z0-9_-` symbols. +// The order of characters is optimized for better gzip and brotli compression. +// References to the same file (works both for gzip and brotli): +// `'use`, `andom`, and `rict'` +// References to the brotli default dictionary: +// `-26T`, `1983`, `40px`, `75px`, `bush`, `jack`, `mind`, `very`, and `wolf` +let urlAlphabet = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + +function nanoid(size = 21) { + let id = ''; + // A compact alternative for `for (var i = 0; i < step; i++)`. + let i = size; + while (i--) { + // `| 0` is more compact and faster than `Math.floor()`. + id += urlAlphabet[(Math.random() * 64) | 0]; + } + return id; +} + +export function App() { + let todos = []; + const todoReducer = (state, action) => { + switch (action.type) { + case ADD_ITEM: + return state.concat({ id: nanoid(), title: action.payload.title, completed: false }); + case UPDATE_ITEM: + return state.map(todo => (todo.id === action.payload.id ? { ...todo, title: action.payload.title } : todo)); + case REMOVE_ITEM: + return state.filter(todo => todo.id !== action.payload.id); + case TOGGLE_ITEM: + return state.map(todo => (todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo)); + case REMOVE_ALL_ITEMS: + return []; + case TOGGLE_ALL: + return state.map(todo => + todo.completed !== action.payload.completed + ? { + ...todo, + completed: action.payload.completed, + } + : todo + ); + case REMOVE_COMPLETED_ITEMS: + return state.filter(todo => !todo.completed); + } + + throw Error(`Unknown action: ${action.type}`); + }; + const dispatch = action => { + todos = todoReducer(todos, action); + }; + + return ( + <> +
+
+