Skip to content

Commit

Permalink
Add support for Iterable in For.
Browse files Browse the repository at this point in the history
  • Loading branch information
nielssp committed Apr 30, 2024
1 parent 87dde1b commit 8bb7636
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 36 deletions.
2 changes: 1 addition & 1 deletion src/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type CellObserver<T> = (newValue: T) => void;
* @category Internals
*/
export type CellProxyObject<T> = T extends {} ? {
[TKey in keyof T]-?: Cell<T[TKey]>;
[TKey in Exclude<keyof T, `@@${string}`>]-?: Cell<T[TKey]>;
} : any;

/**
Expand Down
69 changes: 34 additions & 35 deletions src/for.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { apply } from './component';
import { Context } from './context';
import { ElementChildren } from './types';

function isIterable(x: unknown): x is Iterable<unknown> {
return !!x && Symbol.iterator in Object(x);
}

/**
* Iterate over an array of items and render an element for each. When the cell
* updates the existing DOM-elements will be reused, but an update will be
Expand All @@ -29,7 +33,7 @@ import { ElementChildren } from './types';
* @category Components
*/
export function For<TItem>(props: {
each: Cell<TItem[]>,
each: Cell<Iterable<TItem>>,
children: (value: Cell<TItem>, index: number) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element;
Expand All @@ -50,7 +54,7 @@ export function For<TItem>(props: {
* @param props.else - Alternative to show when array is empty.
*/
export function For<TItem>(props: {
each: TItem[],
each: Iterable<TItem>,
children: (value: TItem, index: number) => ElementChildren,
else?: ElementChildren,
}, context: Context): JSX.Element;
Expand All @@ -77,11 +81,11 @@ export function For<TItem, TKey>(props: {
else?: ElementChildren,
}, context: Context): JSX.Element;
export function For<TItem, TKey>({each, children, else: elseBranch}: {
each: Cell<TItem[]>,
each: Cell<Iterable<TItem>>,
children: (value: Cell<TItem>, index: number) => ElementChildren,
else?: ElementChildren,
} | {
each: TItem[],
each: Iterable<TItem>,
children: (value: TItem, index: number) => ElementChildren,
else?: ElementChildren,
} | {
Expand All @@ -97,29 +101,17 @@ export function For<TItem, TKey>({each, children, else: elseBranch}: {
const cells: MutCell<unknown>[] = [];
context.onInit(() => {
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]) => {
nodes.forEach(node => node.parentElement?.removeChild(node));
nodes.splice(0);
subcontext.destroy();
});
for (let i = 0; i < cells.length; i++) {
cells[i].value = xs[i];
}
} else if (xs.length > cells.length) {
for (let i = 0; i < cells.length; i++) {
cells[i].value = xs[i];
let i = 0;
for (const x of xs) {
if (elseContext) {
elseContext[0].forEach(node => node.parentElement?.removeChild(node));
elseContext[1].destroy();
elseContext = undefined;
}
for (let i = cells.length; i < xs.length; i++) {
if (i >= cells.length) {
const nodes: Node[] = [];
const subcontext = new Context(context);
const c = cell(xs[i]);
const c = cell(x);
cells.push(c);
apply(body(c, i), subcontext).forEach(node => {
if (!marker.parentElement) {
Expand All @@ -130,11 +122,18 @@ export function For<TItem, TKey>({each, children, else: elseBranch}: {
});
items.push([nodes, subcontext]);
subcontext.init();
} else {
cells[i].value = x;
}
} else {
for (let i = 0; i < cells.length; i++) {
cells[i].value = xs[i];
}
i++;
}
if (i < cells.length) {
cells.splice(i);
items.splice(i).forEach(([nodes, subcontext]) => {
nodes.forEach(node => node.parentElement?.removeChild(node));
nodes.splice(0);
subcontext.destroy();
});
}
if (!items.length && elseBranch && marker.parentElement && !elseContext) {
const parent = marker.parentElement;
Expand All @@ -149,20 +148,20 @@ export function For<TItem, TKey>({each, children, else: elseBranch}: {
}
}));
});
} else if (Array.isArray(each)) {
} else if (isIterable(each)) {
const body = children as (value: unknown, index: number) => JSX.Element;
context.onInit(() => {
each.forEach((item, index) => {
const nodes: Node[] = [];
apply(body(item, index), context).forEach(node => {
let i = 0;
for (const item of each) {
apply(body(item, i), context).forEach(node => {
if (!marker.parentElement) {
return;
}
nodes.push(node);
marker.parentElement.insertBefore(node, marker);
});
});
if (!each.length && elseBranch && marker.parentElement) {
i++;
}
if (!i && elseBranch && marker.parentElement) {
const parent = marker.parentElement;
apply(elseBranch, context).forEach(node => {
parent.insertBefore(node, marker);
Expand Down
70 changes: 70 additions & 0 deletions tests/for.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ describe('For', () => {
expect(element.container.textContent).toBe('');
expect(numObservers(a)).toBe(0);
});
it('traverses static maps', () => {
const a = cell(10);
const map = new Map();
map.set('a', 'foo');
map.set('b', 'bar');
map.set('c', 'baz');
const element = mountTest(
<For each={map}>{([key, item], index) =>
<div>{index}: {key}: {item} ({a})</div>
}</For>
);
expect(element.container.textContent).toBe('0: a: foo (10)1: b: bar (10)2: c: baz (10)');

a.value = 5;
expect(element.container.textContent).toBe('0: a: foo (5)1: b: bar (5)2: c: baz (5)');

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

it('shows else branch for empty static arrays', () => {
const a = cell(10);
Expand Down Expand Up @@ -90,6 +110,56 @@ describe('For', () => {
expect(numObservers(a)).toBe(0);
});

it('traverses map cells', () => {
const items = cell(new Map([
['a', 'foo'],
['b', 'bar'],
['c', 'baz'],
]));
const a = cell(10);
const element = mountTest(
<For each={items.map(i => i.values())} else={<div>empty {a}</div>}>{(item, index) =>
<div>{index}: {item} ({a})</div>
}</For>
);
expect(element.container.textContent).toBe('0: foo (10)1: bar (10)2: baz (10)');

items.update(x => x.set('a', 'foobar'));
expect(element.container.textContent).toBe('0: foobar (10)1: bar (10)2: baz (10)');

items.update(x => x.delete('c'));
expect(element.container.textContent).toBe('0: foobar (10)1: bar (10)');

a.value = 5;
expect(element.container.textContent).toBe('0: foobar (5)1: bar (5)');

items.update(x => x.delete('a'));
expect(element.container.textContent).toBe('0: bar (5)');

items.update(x => x.set('d', 'foo'));
expect(element.container.textContent).toBe('0: bar (5)1: foo (5)');

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

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

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

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

element.destroy();

items.update(x => x.set('a', 'foo'));
expect(element.container.textContent).toBe('');

expect(numObservers(items)).toBe(0);
expect(numObservers(a)).toBe(0);
});

it('traverses cell arrays', () => {
const items = cellArray(['foo', 'bar', 'baz']);
const indexed = items.indexed;
Expand Down

0 comments on commit 8bb7636

Please sign in to comment.