diff --git a/.eslintrc.json b/.eslintrc.json index 10269bf..16c730d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,7 @@ "browser": true, "es2021": true }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", @@ -11,10 +11,7 @@ }, "plugins": ["@typescript-eslint", "workspaces"], "rules": { - "indent": ["error", 4], - "linebreak-style": ["error", "unix"], "quotes": ["error", "single"], - "semi": ["error", "always"], "no-var": ["error"], "workspaces/no-relative-imports": "error", "workspaces/require-dependency": "warn" diff --git a/.prettierrc b/.prettierrc index 27512b6..0a79cf3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,8 @@ { - "trailingComma": "es5", + "trailingComma": "all", "tabWidth": 4, - "singleQuote": true + "singleQuote": true, + "printWidth": 100, + "semi": true, + "arrowParens": "always" } diff --git a/package-lock.json b/package-lock.json index f57ce6d..6567512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-workspaces": "^0.10.0", "globals": "^15.1.0", "jest": "^29.7.0", @@ -4490,6 +4491,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-workspaces": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-workspaces/-/eslint-plugin-workspaces-0.10.0.tgz", diff --git a/package.json b/package.json index 2a5cbc8..f4f3006 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-workspaces": "^0.10.0", "globals": "^15.1.0", "jest": "^29.7.0", diff --git a/packages/pipe/src/components/index.ts b/packages/pipe/src/components/index.ts deleted file mode 100644 index bbdc4be..0000000 --- a/packages/pipe/src/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ternary'; diff --git a/packages/pipe/src/components/ternary.spec.ts b/packages/pipe/src/components/ternary.spec.ts deleted file mode 100644 index a792abb..0000000 --- a/packages/pipe/src/components/ternary.spec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { BehaviorSubject, Observable, Subject, map } from 'rxjs'; -import { Component, createElement } from '../createElement'; -import { Ternary } from './ternary'; -import { createRoot } from '../createRoot'; - -const SwitchComponent: Component<{ - bool$: Observable; -}> = ({ bool$ }) => { - return Ternary( - bool$, - () => - createElement('p', { - textContent: new BehaviorSubject('true'), - }), - () => - createElement('p', { - textContent: new BehaviorSubject('false'), - }) - ); -}; - -const CountListenerComponent: Component<{ count$: Observable }> = ({ - count$, -}) => { - return createElement('p', { - textContent: count$, - }); -}; - -const ComplexSwitchComponent: Component<{ - bool$: Observable; - count$: Observable; -}> = ({ bool$, count$ }) => { - return Ternary( - bool$, - () => - createElement('div', {}, [ - createElement(CountListenerComponent, { count$ }), - ]), - () => - createElement('div', {}, [ - createElement(CountListenerComponent, { - count$: count$.pipe(map((count) => count * 2)), - }), - ]) - ); -}; - -const TernaryChildComponent: Component<{ - bool$: Observable; -}> = ({ bool$ }) => { - return createElement('div', {}, [ - createElement(SwitchComponent, { bool$ }), - ]); -}; - -describe('Ternary', () => { - it('should mount the truthy node on initialization', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - - render(createElement(SwitchComponent, { bool$ })); - - const truthyText = container.firstChild as HTMLParagraphElement; - expect(truthyText).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText.textContent).toEqual('true'); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); - - it('should switch to the falsy value when the bool is updated', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - - render(createElement(SwitchComponent, { bool$ })); - - const truthyText = container.firstChild as HTMLParagraphElement; - expect(truthyText).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText.textContent).toEqual('true'); - - expect(container.childNodes.length).toEqual(1); - - bool$.next(false); - - const falsyText = container.firstChild as HTMLParagraphElement; - expect(falsyText).toBeInstanceOf(HTMLParagraphElement); - expect(falsyText.textContent).toEqual('false'); - - expect(container.childNodes.length).toEqual(1); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); - - it('should switch between truthy and falsy values', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - - render(createElement(SwitchComponent, { bool$ })); - - const truthyText = container.firstChild as HTMLParagraphElement; - expect(truthyText).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText.textContent).toEqual('true'); - - expect(container.childNodes.length).toEqual(1); - - bool$.next(false); - - const falsyText = container.firstChild as HTMLParagraphElement; - expect(falsyText).toBeInstanceOf(HTMLParagraphElement); - expect(falsyText.textContent).toEqual('false'); - - expect(container.childNodes.length).toEqual(1); - - bool$.next(true); - - const truthyText2 = container.firstChild as HTMLParagraphElement; - expect(truthyText2).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText2.textContent).toEqual('true'); - - expect(container.childNodes.length).toEqual(1); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); - - it('should not re-create the child if the boolean does not change', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - - render(createElement(SwitchComponent, { bool$ })); - - const truthyText = container.firstChild as HTMLParagraphElement; - expect(truthyText).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText.textContent).toEqual('true'); - - bool$.next(true); - - expect(container.firstChild).toEqual(truthyText); - - bool$.next(false); - - expect(container.firstChild).not.toEqual(truthyText); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); - - it('should render components with children', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - const count$ = new BehaviorSubject(0); - - render(createElement(ComplexSwitchComponent, { bool$, count$ })); - - const truthyDiv = container.firstChild as HTMLDivElement; - expect(truthyDiv).toBeInstanceOf(HTMLDivElement); - - const singleCount = truthyDiv.firstChild as HTMLParagraphElement; - expect(singleCount.textContent).toEqual('0'); - - count$.next(1); - - expect(singleCount.textContent).toEqual('1'); - - expect(container.childNodes.length).toEqual(1); - - bool$.next(false); - - const falsyDiv = container.firstChild as HTMLDivElement; - expect(falsyDiv).toBeInstanceOf(HTMLDivElement); - - const doubleCount = falsyDiv.firstChild as HTMLParagraphElement; - expect(doubleCount.textContent).toEqual('2'); - - count$.next(2); - - expect(doubleCount.textContent).toEqual('4'); - - expect(container.childNodes.length).toEqual(1); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); - - it('should render a Ternary as a child', () => { - const container = document.createElement('div'); - - const { render, unmount } = createRoot(container); - - const bool$ = new Subject(); - - render(createElement(TernaryChildComponent, { bool$ })); - - const div = container.firstChild as HTMLDivElement; - const truthyText = div.firstChild as HTMLParagraphElement; - expect(truthyText).toBeInstanceOf(HTMLParagraphElement); - expect(truthyText.textContent).toEqual('true'); - - bool$.next(false); - - const falsyText = div.firstChild as HTMLParagraphElement; - expect(falsyText).toBeInstanceOf(HTMLParagraphElement); - expect(falsyText.textContent).toEqual('false'); - - unmount(); - - expect(container.firstChild).toBeFalsy(); - }); -}); diff --git a/packages/pipe/src/components/ternary.ts b/packages/pipe/src/components/ternary.ts deleted file mode 100644 index 151e256..0000000 --- a/packages/pipe/src/components/ternary.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { - BehaviorSubject, - Observable, - Subject, - distinctUntilChanged, - takeUntil, - tap, - withLatestFrom, -} from 'rxjs'; -import { PipeNode } from '../createElement'; - -export const Ternary = ( - bool$: Observable, - truthyNode: () => PipeNode, - falsyNode: () => PipeNode -): PipeNode => { - const cleanup$ = new Subject(); - const initialNode = truthyNode(); - const node$ = new BehaviorSubject(initialNode); - - const outNode = { - cleanup$, - element: initialNode.element, - }; - - cleanup$.subscribe(initialNode.cleanup$); - - bool$ - .pipe( - takeUntil(cleanup$), - distinctUntilChanged(), - withLatestFrom(node$), - tap(([bool, node]) => { - const parent = node.element.parentNode; - node.cleanup$.next(); - node.cleanup$.complete(); - - const nextNode = bool ? truthyNode() : falsyNode(); - cleanup$.subscribe(nextNode.cleanup$); - outNode.element = nextNode.element; - parent.appendChild(nextNode.element); - - node$.next(nextNode); - }) - ) - .subscribe(); - - return outNode; -}; diff --git a/packages/pipe/src/createElement.ts b/packages/pipe/src/createElement.ts index f5981e8..0446d3a 100644 --- a/packages/pipe/src/createElement.ts +++ b/packages/pipe/src/createElement.ts @@ -2,7 +2,7 @@ import { Observable, Subject, takeUntil } from 'rxjs'; export type Component>> = ( props: Props, - cleanup$: Observable + cleanup$: Observable, ) => PipeNode; export type PipeNode = { @@ -10,12 +10,10 @@ export type PipeNode = { cleanup$: Subject; }; -export function createElement< - Props extends Record>, ->( +export function createElement>>( component: Component | string, props: Props, - children?: PipeNode[] + children?: PipeNode[] | Observable<[string, PipeNode | null]>, ): PipeNode { const cleanup$ = new Subject(); const element = @@ -29,19 +27,59 @@ export function createElement< }, }); - if (children) { + const node = { element, cleanup$ }; + + if (Array.isArray(children)) { for (const { element: child, cleanup$: childCleanup$ } of children) { element.appendChild(child); cleanup$.subscribe(childCleanup$); } + } else if (children) { + mergeChildNodes(children, node); } - return { element, cleanup$ }; + return node; } -function initializeDomElement< - Props extends Record>, ->(component: string, props: Props, cleanup$: Observable): HTMLElement { +const mergeChildNodes = (source: Observable<[string, PipeNode | null]>, parentNode: PipeNode) => { + const nodes = new Map(); + + source.pipe(takeUntil(parentNode.cleanup$)).subscribe({ + next: ([key, nextNode]) => { + const existingNode = nodes.get(key); + const nextSibling = existingNode?.element.nextSibling; + existingNode?.cleanup$.next(); + existingNode?.cleanup$.complete(); + + if (!nextNode) { + nodes.delete(key); + return; + } + nodes.set(key, nextNode); + parentNode.cleanup$.subscribe(nextNode.cleanup$); + + if (nextSibling) { + parentNode.element.insertBefore(nextNode.element, nextSibling); + return; + } + + parentNode.element.appendChild(nextNode.element); + return; + }, + complete: () => { + for (const node of nodes.values()) { + node.cleanup$.next(); + node.cleanup$.complete(); + } + }, + }); +}; + +function initializeDomElement>>( + component: string, + props: Props, + cleanup$: Observable, +): HTMLElement { const element = document.createElement(component); for (const [key, obs$] of Object.entries(props)) { obs$.pipe(takeUntil(cleanup$)).subscribe((value) => { @@ -52,20 +90,13 @@ function initializeDomElement< return element; } -function initializePipeElement< - Props extends Record>, ->( +function initializePipeElement>>( component: Component, props: Props, - cleanup$: Observable + cleanup$: Observable, ): HTMLElement { const { element, cleanup$: childCleanup$ } = component(props, cleanup$); - cleanup$.subscribe({ - complete: () => { - childCleanup$.next(); - childCleanup$.complete(); - }, - }); + cleanup$.subscribe(childCleanup$); return element; } diff --git a/packages/pipe/src/index.spec.ts b/packages/pipe/src/index.spec.ts index af4dbe1..6e77d7f 100644 --- a/packages/pipe/src/index.spec.ts +++ b/packages/pipe/src/index.spec.ts @@ -1,6 +1,6 @@ import { Component, createElement } from './createElement'; import { createRoot } from './createRoot'; -import { Observable, Subject, BehaviorSubject, scan, map, tap } from 'rxjs'; +import { Observable, Subject, BehaviorSubject, scan, map, tap, distinctUntilChanged } from 'rxjs'; describe('Pipe', () => { it('should render an HTML element in a component', () => { @@ -11,7 +11,7 @@ describe('Pipe', () => { }); const textContent = click$.pipe( scan((x) => x + 1, 0), - map((x) => String(x)) + map((x) => String(x)), ); return createElement('button', { @@ -48,7 +48,7 @@ describe('Pipe', () => { }); const textContent = click$.pipe( scan((x) => x + 1, 0), - map((x) => String(x)) + map((x) => String(x)), ); return createElement('button', { @@ -57,9 +57,7 @@ describe('Pipe', () => { }); }; - const CounterWrapper: Component< - Record> - > = () => { + const CounterWrapper: Component>> = () => { return createElement(Counter, {}); }; const container = document.createElement('div'); @@ -90,7 +88,7 @@ describe('Pipe', () => { }> = ({ count$ }) => { const textContent = count$.pipe( map((x) => String(x)), - tap((value) => spy(value)) + tap((value) => spy(value)), ); return createElement('p', { @@ -115,7 +113,7 @@ describe('Pipe', () => { render( createElement(TextWrapper, { count$, - }) + }), ); const div = container.firstChild as HTMLDivElement; @@ -151,6 +149,138 @@ describe('Pipe', () => { expect(spy).toHaveBeenCalledTimes(4); }); + it('should render a list of children from an observable', () => { + const TextWrapper: Component<{ + addChild$: Observable<[string, string]>; + }> = ({ addChild$ }) => { + return createElement( + 'div', + {}, + addChild$.pipe( + map(([key, value]) => [ + key, + createElement('p', { + textContent: new BehaviorSubject(value), + }), + ]), + ), + ); + }; + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const addChild$ = new Subject<[string, string]>(); + + render( + createElement(TextWrapper, { + addChild$, + }), + ); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeInstanceOf(HTMLDivElement); + + expect(div.textContent).toEqual(''); + + addChild$.next(['a', 'foo']); + + expect(div.textContent).toEqual('foo'); + const childA = div.firstChild as HTMLParagraphElement; + expect(childA.textContent).toEqual('foo'); + + addChild$.next(['b', 'bar']); + + expect(div.textContent).toEqual('foobar'); + const childB = childA.nextSibling as HTMLParagraphElement; + expect(childB.textContent).toEqual('bar'); + + addChild$.next(['a', 'baz']); + + expect(div.textContent).toEqual('bazbar'); + const childAVersion2 = div.firstChild as HTMLParagraphElement; + expect(childAVersion2.textContent).toEqual('baz'); + + expect(childAVersion2.nextSibling).toEqual(childB); + + addChild$.next(['b', null]); + expect(div.textContent).toEqual('baz'); + + addChild$.next(['a', null]); + expect(div.textContent).toEqual(''); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + + it('should pop items from the front of an observable list', () => { + const TextWrapper: Component<{ + addChild$: Observable<[string, string]>; + }> = ({ addChild$ }) => { + return createElement( + 'div', + {}, + addChild$.pipe( + map(([key, value]) => [ + key, + value + ? createElement('p', { + textContent: new BehaviorSubject(value), + }) + : null, + ]), + ), + ); + }; + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const addChild$ = new Subject<[string, string]>(); + + render( + createElement(TextWrapper, { + addChild$, + }), + ); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeInstanceOf(HTMLDivElement); + + expect(div.textContent).toEqual(''); + + addChild$.next(['a', 'foo']); + + expect(div.textContent).toEqual('foo'); + const childA = div.firstChild as HTMLParagraphElement; + expect(childA.textContent).toEqual('foo'); + + addChild$.next(['b', 'bar']); + + expect(div.textContent).toEqual('foobar'); + const childB = childA.nextSibling as HTMLParagraphElement; + expect(childB.textContent).toEqual('bar'); + + addChild$.next(['b', 'baz']); + + expect(div.textContent).toEqual('foobaz'); + const childBVersion2 = div.firstChild.nextSibling as HTMLParagraphElement; + expect(childBVersion2.textContent).toEqual('baz'); + + expect(childBVersion2.previousSibling).toEqual(childA); + + addChild$.next(['a', null]); + expect(div.textContent).toEqual('baz'); + + addChild$.next(['b', null]); + expect(div.textContent).toEqual(''); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + it('should clean up observables on unmount', () => { const spy = jest.fn(); const Text: Component<{ @@ -158,7 +288,7 @@ describe('Pipe', () => { }> = ({ count$ }) => { const textContent = count$.pipe( map((x) => String(x)), - tap((value) => spy(value)) + tap((value) => spy(value)), ); return createElement('p', { @@ -180,7 +310,7 @@ describe('Pipe', () => { render( createElement(TextWrapper, { count$, - }) + }), ); const textElement = container.firstChild as HTMLParagraphElement; @@ -209,3 +339,225 @@ describe('Pipe', () => { expect(spy).toHaveBeenCalledWith('2'); }); }); + +describe('Boolean Operator', () => { + const SwitchComponent: Component<{ + bool$: Observable; + }> = ({ bool$ }) => { + return createElement( + 'div', + {}, + bool$.pipe( + map((value) => [ + 'default', + value + ? createElement('p', { + textContent: new BehaviorSubject('true'), + }) + : createElement('p', { + textContent: new BehaviorSubject('false'), + }), + ]), + ), + ); + }; + + const CountListenerComponent: Component<{ count$: Observable }> = ({ count$ }) => { + return createElement('p', { + textContent: count$, + }); + }; + + const ComplexSwitchComponent: Component<{ + bool$: Observable; + count$: Observable; + }> = ({ bool$, count$ }) => { + return createElement( + 'div', + {}, + bool$.pipe( + distinctUntilChanged(), + map((value) => [ + 'default', + value + ? createElement(CountListenerComponent, { count$ }) + : createElement(CountListenerComponent, { + count$: count$.pipe(map((count) => count * 2)), + }), + ]), + ), + ); + }; + + it('should not mount a node on initialization', () => { + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const bool$ = new Subject(); + + render(createElement(SwitchComponent, { bool$ })); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeTruthy(); + expect(div.firstChild).toBeFalsy(); + + bool$.next(true); + + const truthyText = div.firstChild as HTMLParagraphElement; + expect(truthyText).toBeInstanceOf(HTMLParagraphElement); + expect(truthyText.textContent).toEqual('true'); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + + it('should switch to the falsy value when the bool is updated', () => { + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const bool$ = new Subject(); + + render(createElement(SwitchComponent, { bool$ })); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeTruthy(); + expect(div.firstChild).toBeFalsy(); + + bool$.next(true); + + const truthyText = div.firstChild as HTMLParagraphElement; + expect(truthyText).toBeInstanceOf(HTMLParagraphElement); + expect(truthyText.textContent).toEqual('true'); + + expect(div.childNodes.length).toEqual(1); + + bool$.next(false); + + const falsyText = div.firstChild as HTMLParagraphElement; + expect(falsyText).toBeInstanceOf(HTMLParagraphElement); + expect(falsyText.textContent).toEqual('false'); + + expect(div.childNodes.length).toEqual(1); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + + it('should switch between truthy and falsy values', () => { + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const bool$ = new Subject(); + + render(createElement(SwitchComponent, { bool$ })); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeTruthy(); + expect(div.firstChild).toBeFalsy(); + + bool$.next(true); + + const truthyText = div.firstChild as HTMLParagraphElement; + expect(truthyText).toBeInstanceOf(HTMLParagraphElement); + expect(truthyText.textContent).toEqual('true'); + + expect(div.childNodes.length).toEqual(1); + + bool$.next(false); + + const falsyText = div.firstChild as HTMLParagraphElement; + expect(falsyText).toBeInstanceOf(HTMLParagraphElement); + expect(falsyText.textContent).toEqual('false'); + + expect(div.childNodes.length).toEqual(1); + + bool$.next(true); + + const truthyText2 = div.firstChild as HTMLParagraphElement; + expect(truthyText2).toBeInstanceOf(HTMLParagraphElement); + expect(truthyText2.textContent).toEqual('true'); + + expect(div.childNodes.length).toEqual(1); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + + it('should not re-create the child if the boolean does not change', () => { + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const bool$ = new Subject(); + + render(createElement(SwitchComponent, { bool$ })); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeTruthy(); + expect(div.firstChild).toBeFalsy(); + + bool$.next(true); + + const truthyText = div.firstChild as HTMLParagraphElement; + expect(truthyText).toBeInstanceOf(HTMLParagraphElement); + expect(truthyText.textContent).toEqual('true'); + + bool$.next(true); + + expect(div.firstChild).toEqual(truthyText); + + bool$.next(false); + + expect(div.firstChild).not.toEqual(truthyText); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); + + it('should render components with children', () => { + const container = document.createElement('div'); + + const { render, unmount } = createRoot(container); + + const bool$ = new Subject(); + const count$ = new BehaviorSubject(0); + + render(createElement(ComplexSwitchComponent, { bool$, count$ })); + + const div = container.firstChild as HTMLDivElement; + expect(div).toBeInstanceOf(HTMLDivElement); + + bool$.next(true); + + const singleCount = div.firstChild as HTMLParagraphElement; + expect(singleCount.textContent).toEqual('0'); + + count$.next(1); + + expect(singleCount.textContent).toEqual('1'); + + expect(container.childNodes.length).toEqual(1); + + bool$.next(false); + + const doubleCount = div.firstChild as HTMLParagraphElement; + expect(doubleCount.textContent).toEqual('2'); + + count$.next(2); + + expect(doubleCount.textContent).toEqual('4'); + + expect(container.childNodes.length).toEqual(1); + + unmount(); + + expect(container.firstChild).toBeFalsy(); + }); +}); diff --git a/packages/pipe/src/index.ts b/packages/pipe/src/index.ts index 6dede37..356151c 100644 --- a/packages/pipe/src/index.ts +++ b/packages/pipe/src/index.ts @@ -1,3 +1,2 @@ export * from './createElement'; export * from './createRoot'; -export * from './components';