Skip to content

Commit

Permalink
Add else-branch to <For>. Add more documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
nielssp committed Apr 30, 2024
1 parent 6d81778 commit 87dde1b
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 42 deletions.
91 changes: 89 additions & 2 deletions src/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,17 @@ export function Unwrap<T>(props: {
}

/**
* Use this component for lazy importing other components.
*
* @example
* ```tsx
* <Lazy else='loading...'>{() => import('./my-component').then(m => <m.MyComponent/>)}</Lazy>
* ```
*
* @param props.children - Async function for that resolves to an element.
* @param props.else - Element to show until the promise resolves.
* @param props.onError - Error handler which is executed if the promise is
* rejected.
* @category Components
*/
export function Lazy(props: {
Expand Down Expand Up @@ -715,6 +726,23 @@ export function Lazy(props: {
}

/**
* Dynamically replace elements based on the value of a cell.
*
* @example
* ```tsx
* function MyComponent(props: {
* foo: number,
* }) {
* return <div>{props.foo}</div>;
* }
*
* const comp = cell(MyComponent);
*
* <Dynamic component={comp} foo={5}/>
* ```
*
* @param props.component Component to render.
* @param props.else Element to render when `props.component` is undefined.
* @category Components
*/
export function Dynamic<T>(props: T & {
Expand Down Expand Up @@ -764,6 +792,17 @@ export function Dynamic<T>(props: T & {
}

/**
* Apply style properties to one or more child elements.
*
* @example
* ```tsx
* <Style backgroundColor='blue' color='green'>
* <div>test</div>
* </Style>
* ```
*
* @param props.children - The elements to apply styling to.
* @param props - CSS style properties.
* @category Components
*/
export function Style(props: {
Expand Down Expand Up @@ -812,13 +851,35 @@ export function mount(container: HTMLElement, ... elements: JSX.Element[]): () =
}

/**
* JSX fragment factory.
*
* @example
* ```tsx
* <>
* <span>first</span>
* <span>second</span>
* </>
* ```
*
* @param props.children - Child elements.
* @category Components
*/
export function Fragment({children}: {children: ElementChildren}): JSX.Element {
return flatten(children);
export function Fragment(props: {children: ElementChildren}): JSX.Element {
return flatten(props.children);
}

/**
* Attach an observer to the lifecycle of the current {@link Context}. The
* observer will be detached when the context is destroyed.
*
* @example
* ```tsx
* <Observe from={emitter} then={event => doSomething(event)}/>
* ```
*
* @param props
* @param props.from - An observable (e.g. a {@link Cell} or an {@link Emitter}).
* @param props.then - The event handler.
* @category Components
*/
export function Observe<TEvent>({from, then}: {
Expand All @@ -833,6 +894,19 @@ export function Observe<TEvent>({from, then}: {
}

/**
* Attach a window listener to the lifecycle of the current {@link Context}. The
* event listener will be removed when the context is destroyed.
*
* @example
* ```tsx
* <WindowListener on='resize' then={e => handleWindowResizeEvent(e)}/>
* ```
*
* @param props
* @param props.on - Event name.
* @param props.then - Event handler.
* @param props.capture - Whether to use event capturing.
* @param props.options - Additional event listener options.
* @category Components
*/
export function WindowListener<TKey extends keyof WindowEventMap>({on, then, capture, options}: {
Expand All @@ -849,6 +923,19 @@ export function WindowListener<TKey extends keyof WindowEventMap>({on, then, cap
}

/**
* Attach a document listener to the lifecycle of the current {@link Context}. The
* event listener will be removed when the context is destroyed.
*
* @example
* ```tsx
* <DocumentListener on='keydown' then={e => handleKeydownEvent(e)}/>
* ```
*
* @param props
* @param props.on - Event name.
* @param props.then - Event handler.
* @param props.capture - Whether to use event capturing.
* @param props.options - Additional event listener options.
* @category Components
*/
export function DocumentListener<TKey extends keyof DocumentEventMap>({on, then, capture, options}: {
Expand Down
101 changes: 67 additions & 34 deletions src/for.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// http://opensource.org/licenses/MIT for more information.

import { CellIterable } from './array';
import { Cell, cell } from './cell';
import { Cell, MutCell, cell } from './cell';
import { apply } from './component';
import { Context } from './context';
import { ElementChildren } from './types';
Expand All @@ -25,11 +25,13 @@ import { ElementChildren } from './types';
* @param props.each - A cell containing an array of items to iterate over.
* @param props.children - A function that accepts an item cell and returns an
* element.
* @param props.else - Alternative to show when array is empty.
* @category Components
*/
export function For<TItem>(props: {
each: Cell<TItem[]>,
children: (value: Cell<TItem>, index: number) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element;
/**
* Iterate over a static array of items and render an element for each.
Expand All @@ -45,10 +47,12 @@ export function For<TItem>(props: {
* @param props.each - An array of items to iterate over.
* @param props.children - A function that accepts an item and returns an
* element.
* @param props.else - Alternative to show when array is empty.
*/
export function For<TItem>(props: {
each: TItem[],
children: (value: TItem, index: number) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element;
/**
* Iterate over a {@link CellIterable} of items and keys. See {@link cellArray}
Expand All @@ -65,41 +69,39 @@ export function For<TItem>(props: {
* @param props.each - An iterable to itearate over.
* @param props.children - A function that accepts an item cell and returns an
* element.
* @param props.else - Alternative to show when array is empty.
*/
export function For<TItem, TKey>(props: {
each: CellIterable<TItem, TKey>,
children: (value: TItem, key: TKey) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element;
export function For<TItem, TKey>({each, children}: {
each: Cell<TItem[]>,
export function For<TItem, TKey>({each, children, else: elseBranch}: {
each: Cell<TItem[]>,
children: (value: Cell<TItem>, index: number) => ElementChildren,
else?: ElementChildren,
} | {
each: TItem[],
each: TItem[],
children: (value: TItem, index: number) => ElementChildren,
else?: ElementChildren,
} | {
each: CellIterable<TItem, TKey>,
each: CellIterable<TItem, TKey>,
children: (value: TItem, key: TKey) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element {
const marker = document.createComment('<For>');
let items: [Node[], Context][] = [];
let elseContext: [Node[], Context] | undefined;
if (each instanceof Cell) {
const body = children as (value: Cell<unknown>, index: number) => JSX.Element;
const cells = each.value.map(v => cell(v));
const cells: MutCell<unknown>[] = [];
context.onInit(() => {
cells.forEach((item, index) => {
const nodes: Node[] = [];
const subcontext = new Context(context);
apply(body(item, index), subcontext).forEach(node => {
if (!marker.parentElement) {
return;
}
nodes.push(node);
marker.parentElement.insertBefore(node, marker);
});
items.push([nodes, subcontext]);
subcontext.init();
});
context.onDestroy(each.observe(xs => {
context.onDestroy(each.getAndObserve(xs => {
if (xs.length && elseContext) {
elseContext[0].forEach(node => node.parentElement?.removeChild(node));
elseContext[1].destroy();
elseContext = undefined;
}
if (xs.length < cells.length) {
cells.splice(xs.length);
items.splice(xs.length).forEach(([nodes, subcontext]) => {
Expand Down Expand Up @@ -134,14 +136,18 @@ export function For<TItem, TKey>({each, children}: {
cells[i].value = xs[i];
}
}
if (!items.length && elseBranch && marker.parentElement && !elseContext) {
const parent = marker.parentElement;
const subcontext = new Context(context);
const childNodes: Node[] = [];
apply(elseBranch, subcontext).forEach(node => {
parent.insertBefore(node, marker);
childNodes.push(node);
});
subcontext.init();
elseContext = [childNodes, subcontext];
}
}));
context.onDestroy(() => {
items.forEach(([nodes, subcontext]) => {
nodes.forEach(node => node.parentElement?.removeChild(node));
nodes.splice(0);
subcontext.destroy();
});
});
});
} else if (Array.isArray(each)) {
const body = children as (value: unknown, index: number) => JSX.Element;
Expand All @@ -156,12 +162,23 @@ export function For<TItem, TKey>({each, children}: {
marker.parentElement.insertBefore(node, marker);
});
});
if (!each.length && elseBranch && marker.parentElement) {
const parent = marker.parentElement;
apply(elseBranch, context).forEach(node => {
parent.insertBefore(node, marker);
});
}
});
} else {
const body = children as (value: unknown, key: unknown) => JSX.Element;
context.onInit(() => {
context.onDestroy(each.observe(
(index, item, key) => {
if (elseContext) {
elseContext[0].forEach(node => node.parentElement?.removeChild(node));
elseContext[1].destroy();
elseContext = undefined;
}
const subcontext = new Context(context);
if (index >= items.length) {
const nodes: Node[] = [];
Expand Down Expand Up @@ -199,16 +216,32 @@ export function For<TItem, TKey>({each, children}: {
nodes.splice(0);
subcontext.destroy();
});
if (!items.length && elseBranch && marker.parentElement && !elseContext) {
const parent = marker.parentElement;
const subcontext = new Context(context);
const childNodes: Node[] = [];
apply(elseBranch, subcontext).forEach(node => {
parent.insertBefore(node, marker);
childNodes.push(node);
});
subcontext.init();
elseContext = [childNodes, subcontext];
}
},
));
context.onDestroy(() => {
items.forEach(([nodes, subcontext]) => {
nodes.forEach(node => node.parentElement?.removeChild(node));
nodes.splice(0);
subcontext.destroy();
});
});
});
}
context.onDestroy(() => {
if (elseContext) {
elseContext[0].forEach(node => node.parentElement?.removeChild(node));
elseContext[1].destroy();
elseContext = undefined;
}
items.forEach(([nodes, subcontext]) => {
nodes.forEach(node => node.parentElement?.removeChild(node));
nodes.splice(0);
subcontext.destroy();
});
});
return () => marker;
}
42 changes: 40 additions & 2 deletions tests/for.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,28 @@ describe('For', () => {
expect(numObservers(a)).toBe(0);
});

it('shows else branch for empty static arrays', () => {
const a = cell(10);
const element = mountTest(
<For each={[]} else={<div>empty {a}</div>}>{(item, index) =>
<div>{index}: {item} ({a})</div>
}</For>
);
expect(element.container.textContent).toBe('empty 10');

a.value = 5;
expect(element.container.textContent).toBe('empty 5');

element.destroy();
expect(element.container.textContent).toBe('');
expect(numObservers(a)).toBe(0);
});

it('traverses array cells', () => {
const items = cell(['foo', 'bar', 'baz']);
const a = cell(10);
const element = mountTest(
<For each={items}>{(item, index) =>
<For each={items} else={<div>empty {a}</div>}>{(item, index) =>
<div>{index}: {item} ({a})</div>
}</For>
);
Expand All @@ -52,6 +69,18 @@ describe('For', () => {
items.update(x => x.unshift('baz'));
expect(element.container.textContent).toBe('0: baz (5)1: bar (5)2: foo (5)');

items.update(x => x.splice(0));
expect(element.container.textContent).toBe('empty 5');

a.value = 10;
expect(element.container.textContent).toBe('empty 10');

items.update(x => x.push('baz'));
expect(element.container.textContent).toBe('0: baz (10)');

items.update(x => x.splice(0));
expect(element.container.textContent).toBe('empty 10');

element.destroy();

items.update(x => x.push('foo'));
Expand All @@ -66,7 +95,7 @@ describe('For', () => {
const indexed = items.indexed;
const a = cell(10);
const element = mountTest(
<For each={indexed}>{(item, index) =>
<For each={indexed} else={<div>empty {a}</div>}>{(item, index) =>
<div>{index}: {item} ({a})</div>
}</For>
);
Expand All @@ -90,6 +119,15 @@ describe('For', () => {
items.insert(0, 'baz');
expect(element.container.textContent).toBe('0: baz (5)1: bar (5)2: foo (5)');

items.clear();
expect(element.container.textContent).toBe('empty 5');

a.value = 10;
expect(element.container.textContent).toBe('empty 10');

items.push('foo');
expect(element.container.textContent).toBe('0: foo (10)');

element.destroy();

items.push('foo');
Expand Down
Loading

0 comments on commit 87dde1b

Please sign in to comment.