diff --git a/packages/core/src/component/Show.spec.tsx b/packages/core/src/component/Show.spec.tsx deleted file mode 100644 index 0801147..0000000 --- a/packages/core/src/component/Show.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// /* eslint-disable @typescript-eslint/no-explicit-any */ -// import { describe, expect, it, vi } from "vitest"; -// import { buildMockParent } from "../test-utils/dom-element.mock"; -// import { render } from "../render"; -// import { component } from './v-component'; -// import type { VComponent } from "./component"; -// import { bool } from "../reactive"; -// import { Show } from "./Show"; - - -// // function setup({ list }: { list: ReturnType> }) { -// // const renderFn = vi.fn((item: string) => item); - - - -// // const mockParent = buildMockParent(); - -// // let cmp!: ReturnType; - -// // const mount = () => { -// // cmp = TestComponent({}); - -// // render(cmp, mockParent.html); -// // }; - -// // const hasChildren = (children: any[]) => { -// // expect(cmp.parent.html.childNodes).toEqual(children); -// // }; - -// // const shouldHaveRenderedNChildren = (n: number) => { -// // expect(renderFn).toHaveBeenCalledTimes(n); -// // }; - -// // return { shouldHaveRenderedNChildren, mount, hasChildren }; -// // } - -// function mountComponent(component: VComponent) { -// const mockParent = buildMockParent(); - -// render(component, mockParent.html) - - -// const hasChildren = (children: any[]) => { -// expect(component.parent.html.childNodes).toEqual(children); -// }; - -// return { hasChildren } - -// } - - -// describe('', () => { - -// it('renders the right child initially', () => { - - -// const Card = component(({ children}) => { -// return
children
-// }) - -// const TestComponent = component(() => { -// const [condition, toggle ] = bool(true); - -// return -// a -// -// }) - - -// const { hasChildren } = mountComponent() - -// hasChildren(['a']) - - -// }) -// }) \ No newline at end of file diff --git a/packages/core/src/component/SimpleSwitch.spec.tsx b/packages/core/src/component/SimpleSwitch.spec.tsx new file mode 100644 index 0000000..f1a88ab --- /dev/null +++ b/packages/core/src/component/SimpleSwitch.spec.tsx @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { describe, expect, it } from 'vitest'; +import { Match, Switch } from './SimpleSwitch'; +import { component } from '.'; +import { reactive } from '../reactive'; +import { fakeMount } from '../test-utils/fake-mount'; +import type { ComponentChildren } from '../dom/create-dom-element'; + +describe('Switch/Match', () => { + it('initially renders the correct child ', () => { + const condition = reactive('a'); + + const TestComponent = component(() => { + return ( + + a + b + c + + ); + }); + + const { children, cmp } = fakeMount(TestComponent); + expect(children).toEqual(['a']); + }); + + it('renders the right child when the condition changes', () => { + const condition = reactive('a'); + + const TestComponent = component(() => { + return ( + + a + b + c + + ); + }); + + const { children } = fakeMount(TestComponent); + + condition.update('b'); + + expect(children).toEqual(['b']); + }); + + it('renders the fallback when no child matches the condition', () => { + const condition = reactive('no match'); + + // TODO: need to find a good way to easily render a string + //@ts-expect-error + const Fallback = component((props) => { + return props.children || [] as ComponentChildren + }) + + const TestComponent = component(() => { + + + + + return ( + fallback} > + a + b + c + + ); + }); + + const { children } = fakeMount(TestComponent); + + + expect(children).toEqual(['fallback']); + }); +}); diff --git a/packages/core/src/component/SimpleSwitch.ts b/packages/core/src/component/SimpleSwitch.ts new file mode 100644 index 0000000..d457737 --- /dev/null +++ b/packages/core/src/component/SimpleSwitch.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { + buildSwitchComponent, + component, + type ComponentFn, + type VComponent, +} from '.'; +import type { PrimitiveType } from '../utils/primitive'; +import { isVComponent } from './is-component'; + +// TODO: fix types here + +type ComponentProps = Parameters>[0]; + +export function Match( + props: ComponentProps<{ when: T }> +) { + //@ts-expect-error + const componentFn = component<{ when: T }>(({ children }) => { + return children; + }); + + return componentFn(props); +} + +export function Switch( + props: ComponentProps<{ condition: T; fallback?: VComponent }> +) { + const componentFn = component<{ condition: T; fallback?: VComponent }>( + ({ children, condition, fallback }) => { + // TODO: fix types here + //@ts-expect-error + return buildSwitchComponent(condition, (value) => { + for (const child of children || []) { + if (!isVComponent(child)) return; + + const childMatchesCondition = + 'props' in child && + !!child.props && + typeof child.props === 'object' && + 'when' in child.props && + child.props.when === value; + + // TODO: need to add props as type of VComponent ? + if (childMatchesCondition) { + console.log({ child }); + return child; + } + } + + return fallback; + }); + } + ); + + return componentFn(props); +} diff --git a/packages/core/src/component/for-loop.spec.tsx b/packages/core/src/component/for-loop.spec.tsx index 13248f8..a89311c 100644 --- a/packages/core/src/component/for-loop.spec.tsx +++ b/packages/core/src/component/for-loop.spec.tsx @@ -3,8 +3,7 @@ import { describe, expect, it, vi } from 'vitest'; import { component } from './v-component'; import { reactiveList } from '../reactive'; import { For } from './for-loop'; -import { buildMockParent } from '../test-utils/dom-element.mock'; -import { render } from '../render'; +import { fakeMount } from '../test-utils/fake-mount'; function setup({ list }: { list: ReturnType> }) { const renderFn = vi.fn((item: string) => item); @@ -13,18 +12,19 @@ function setup({ list }: { list: ReturnType> }) { return {renderFn}; }); - const mockParent = buildMockParent(); - let cmp!: ReturnType; + let cmpChildren: ReturnType['children']; const mount = () => { - cmp = TestComponent({}); - render(cmp, mockParent.html); + const { children } = fakeMount(TestComponent) + + cmpChildren = children; + }; const hasChildren = (children: any[]) => { - expect(cmp.parent.html.childNodes).toEqual(children); + expect(cmpChildren).toEqual(children); }; const shouldHaveRenderedNChildren = (n: number) => { diff --git a/packages/core/src/component/index.ts b/packages/core/src/component/index.ts index dfb55c0..148b62f 100644 --- a/packages/core/src/component/index.ts +++ b/packages/core/src/component/index.ts @@ -5,3 +5,4 @@ export { type VComponent } from './component'; export { buildSwitchComponent } from './switch'; export { onDestroy, onInit } from './lifecycle-hooks'; +export { Switch, Match } from './SimpleSwitch'; diff --git a/packages/core/src/component/is-component.ts b/packages/core/src/component/is-component.ts index 0ceac13..59117c7 100644 --- a/packages/core/src/component/is-component.ts +++ b/packages/core/src/component/is-component.ts @@ -2,6 +2,7 @@ import type { MaybeReactive } from '../reactive'; import type { VComponent } from './component'; export function isVComponent( + // eslint-disable-next-line @typescript-eslint/no-explicit-any cmp: VComponent | MaybeReactive | HTMLElement | Comment ): cmp is VComponent { return ( diff --git a/packages/core/src/component/render-new-nodes.ts b/packages/core/src/component/render-new-nodes.ts index 2347f3c..d43a813 100644 --- a/packages/core/src/component/render-new-nodes.ts +++ b/packages/core/src/component/render-new-nodes.ts @@ -1,3 +1,4 @@ +import { logger } from '../common'; import { type MaybeArray, toArray } from '../utils/array'; import { type WithHtml } from './component'; @@ -15,12 +16,42 @@ export function removeOldNodesAndRenderNewNodes({ const firstNode = toArray(oldNodes)[0]; const firstNodeIndex = allChildren.findIndex((n) => n === firstNode); - toArray(oldNodes).forEach((node) => parent.html.removeChild(node)); + toArray(oldNodes).forEach((node) => { + safelyRemoveChild(parent, node); + }); toArray(newNodes).forEach((newNode, index) => { + safelyInsertNode(parent, firstNodeIndex, index, newNode); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function safelyRemoveChild(parent: WithHtml, node: any) { + try { + parent.html.removeChild(node); + } catch (err) { + logger.warn('Error while removing child '); + console.debug({ node }); + throw new Error('[TINAF] Error while removing child', { cause: err }); + } +} + +function safelyInsertNode( + parent: WithHtml, + firstNodeIndex: number, + index: number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + newNode: any +) { + try { parent.html.insertBefore( newNode, [...parent.html.childNodes][firstNodeIndex + index] ); - }); + } catch (err) { + logger.warn('Error while adding node ', newNode); + console.debug({ newNode }); + + throw new Error('[TINAF] Error while adding child', { cause: err }); + } } diff --git a/packages/core/src/component/switch.ts b/packages/core/src/component/switch.ts index 21f4647..e635618 100644 --- a/packages/core/src/component/switch.ts +++ b/packages/core/src/component/switch.ts @@ -1,12 +1,12 @@ -import { isReactive, toValue, type ReactiveValue } from '../reactive'; +import { isReactive, toValue, type MaybeReactive } from '../reactive'; import type { VComponent, WithHtml } from './component'; -import { Subscription, distinctUntilChanged, skip, startWith, tap } from 'rxjs'; +import { Subscription, distinctUntilChanged, skip, startWith } from 'rxjs'; import { removeOldNodesAndRenderNewNodes } from './render-new-nodes'; import type { AddClassesArgs } from '../dom/create-dom-element'; class SwitchComponent implements VComponent { constructor( - private reactiveValue: ReactiveValue, + private reactiveValue: MaybeReactive, private switchFn: (value: T) => VComponent | null, private comparisonFn?: (a: T, b: T) => boolean, private onDestroy?: () => void @@ -99,7 +99,7 @@ class SwitchComponent implements VComponent { } export function buildSwitchComponent( - reactiveValue: ReactiveValue, + reactiveValue: MaybeReactive, switchFn: (value: T) => VComponent | null, { comparisonFn, diff --git a/packages/core/src/component/v-component.ts b/packages/core/src/component/v-component.ts index 203eac5..ef726d1 100644 --- a/packages/core/src/component/v-component.ts +++ b/packages/core/src/component/v-component.ts @@ -5,7 +5,7 @@ import type { ComponentChildren, } from '../dom/create-dom-element'; import { type MaybeReactive } from '../reactive'; -import { type MaybeArray } from '../utils/array'; +import { toArray, type MaybeArray } from '../utils/array'; import { type TinafElement, type VComponent, type WithHtml } from './component'; import { isVComponent } from './is-component'; import { @@ -49,12 +49,12 @@ export class SimpleVComponent this._registerOnDestroyCallback(); - if (isVComponent(this.child)) { - this.child.init(this.parent); - this.child.addClass(this.classesUnion); - } else { - this.html = this.child; - } + this.children.forEach((child) => { + if (isVComponent(child)) { + child.init(this.parent); + child.addClass(this.classesUnion); + } + }); this._executeOnInitCallback(); } @@ -71,13 +71,21 @@ export class SimpleVComponent if (lastOnDestroyCallback) this.onDestroyCallback = lastOnDestroyCallback; } + private get children() { + return toArray(this.child); + } renderOnce(): MaybeArray { - if (isVComponent(this.child)) { - const html = this.child.renderOnce(); - this.html = html; - return html; - } - return this.child; + const newHtml = this.children.map((child) => { + if (isVComponent(child)) { + return child.renderOnce(); + } + + return child; + }); + + this.html = newHtml.flat(); + + return this.html; } destroy(): void { diff --git a/packages/core/src/test-utils/fake-mount.ts b/packages/core/src/test-utils/fake-mount.ts new file mode 100644 index 0000000..4b4accf --- /dev/null +++ b/packages/core/src/test-utils/fake-mount.ts @@ -0,0 +1,17 @@ +import type { VComponent } from '../component'; +import { render } from '../render'; +import { buildMockParent } from './dom-element.mock'; + +// eslint-disable-next-line @typescript-eslint/ban-types +export const fakeMount = (component: (props: {}) => VComponent) => { + const cmp = component({}); + + const mockParent = buildMockParent(); + + render(cmp, mockParent.html); + + return { + cmp, + children: cmp.parent.html.childNodes, + }; +}; diff --git a/packages/demo-todo-app/src/App.tsx b/packages/demo-todo-app/src/App.tsx index b7d80d7..86e2a09 100644 --- a/packages/demo-todo-app/src/App.tsx +++ b/packages/demo-todo-app/src/App.tsx @@ -1,5 +1,6 @@ import { component, + Switch, } from 'tinaf/component'; import { div } from 'tinaf/dom'; import { Header } from './Header/Header'; @@ -8,8 +9,9 @@ import { RouterView, type PageComponent } from 'tinaf/router'; import { Link } from './ui/Link'; import { Example } from './tests/Example'; import { ShowExample } from './examples/ShowExample'; -import { inputReactive } from 'tinaf/reactive'; +import { inputReactive, reactive } from 'tinaf/reactive'; import { useTimeout } from '../../core/src/common-hooks'; +import { Match } from '../../core/src/component'; const MainContainer = component(({ children }) => { return div(...(children || [])).addClass('p-8 gap-8 flex flex-col '); @@ -23,6 +25,17 @@ export const App: PageComponent = component(() => { // useTimeout(() => placeholder.update("world"), 1000) + const condition = reactive('a') + + + const updateCondition = () => { + const rand = Math.random(); + if (rand > .7) condition.update('a') + else if (rand > .4) condition.update('b') + else condition.update('c') + } + + return
@@ -47,5 +60,27 @@ export const App: PageComponent = component(() => { + + + + + + + +{/* note: wrapping the elements in a html-like component is necessary for the moment */} + +
a
+
+ + +
b
+
+ + +
c
+
+ +
+
});