Skip to content

Commit

Permalink
Add more documentation. Improve type of For.
Browse files Browse the repository at this point in the history
  • Loading branch information
nielssp committed Apr 29, 2024
1 parent e743787 commit 6d81778
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 36 deletions.
132 changes: 114 additions & 18 deletions src/array.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,29 @@ export function cellMap<TKey, TValue>(initialEntries: Iterable<[TKey, TValue]> =
}

/**
* Common interface for data structures that can be passed to {@link For}.
*
* @category Cell streams and arrays
*/
export interface CellIterable<TValue, TKey> {
/**
* Observe this iterable. Upon attaching an observer `insert` is called for
* each item currently contained in the underlying collection.
*
* @param insert - An observer that is called whenever an item is inserted.
* @param remove - An observer that is called whenever an item is removed.
* @returns A function that should be called to detach the observer
* functions.
*/
observe(
insert: (index: number, item: TValue, key: TKey) => void,
remove: (index: number) => void,
): () => void;
}

/**
* Common base class for collections of cells that can be mapped and filtered.
*
* @category Cell streams and arrays
*/
export abstract class CellStream<TItem, TKey> implements CellIterable<Cell<TItem>, TKey> {
Expand All @@ -46,18 +59,40 @@ export abstract class CellStream<TItem, TKey> implements CellIterable<Cell<TItem
remove: (index: number) => void,
): () => void;

/**
* Create a strem that applies a function to each item emitted by this
* stream.
*
* @param f - The function to apply to items.
* @returns A new stream.
*/
map<TOut>(f: (item: TItem, key: TKey) => TOut): CellStream<TOut, TKey> {
return new CellStreamWrapper((insert, remove) => {
return this.observe((index, item, key) => insert(index, item.map(v => f(v, key)), key), remove);
});
}

/**
* Create a strem that applies a function to the key of each item
* emitted by this stream.
*
* @param f - The function to apply to items. The returned value will be the
* new key.
* @returns A new stream.
*/
mapKey<TOut>(f: (item: Cell<TItem>, key: TKey) => TOut): CellStream<TItem, TOut> {
return new CellStreamWrapper((insert, remove) => {
return this.observe((index, item, key) => insert(index, item, f(item, key)), remove);
});
}

/**
* Create a stream that only includes items from this stream for which the
* given predicate function returns true.
*
* @param f - The predicate function to apply to items.
* @returns A new stream.
*/
filter(f: (item: TItem) => boolean): CellStream<TItem, TKey> {
return new CellStreamWrapper((insert, remove) => {
type FilteredItem = {unobserve?: () => void, mappedIndex: number};
Expand Down Expand Up @@ -128,6 +163,12 @@ export abstract class CellStream<TItem, TKey> implements CellIterable<Cell<TItem
});
}

/**
* Create a stream that assigns an index, starting at 0, to eachitem in this stream.
* The indices are cells that will update when items are removed.
*
* @returns A new stream.
*/
get indexed(): CellStream<TItem, Cell<number>> {
return new CellStreamWrapper((insert, remove) => {
const indices: MutCell<number>[] = [];
Expand All @@ -153,9 +194,6 @@ export abstract class CellStream<TItem, TKey> implements CellIterable<Cell<TItem
}
}

/**
* @category Cell streams and arrays
*/
class CellStreamWrapper<TItem, TKey> extends CellStream<TItem, TKey> {
constructor(
private f: (
Expand All @@ -175,20 +213,42 @@ class CellStreamWrapper<TItem, TKey> extends CellStream<TItem, TKey> {
}

/**
* A dynamic array of mutable cells.
*
* @category Cell streams and arrays
*/
export class CellArray<TItem> extends CellStream<TItem, void> {
private readonly cells: MutCell<MutCell<TItem>[]> = cell(Array.from(this.initialItems).map(item => cell(item)));
private _onInsert = createEmitter<{index: number, item: Cell<TItem>}>();
private _onRemove = createEmitter<number>();

/**
* The number of items in the array.
*/
readonly length = this.cells.map(cells => cells.length);
readonly onInsert = createEmitter<{index: number, item: Cell<TItem>}>();
readonly onRemove = createEmitter<number>();

/**
* An emitter that emits events when items are inserted.
*/
readonly onInsert: Emitter<{index: number, item: Cell<TItem>}> = this._onInsert.asEmitter();

/**
* An emitter that emits events when items are removed.
*/
readonly onRemove: Emitter<number> = this._onRemove.asEmitter();

/**
* @param initialItems - The initial items of the array.
*/
constructor(
private initialItems: Iterable<TItem>,
) {
super()
}

/**
* All items of the array as mutable cells.
*/
get items(): MutCell<TItem>[] {
return this.cells.value;
}
Expand All @@ -198,10 +258,10 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
remove: (index: number) => void,
): () => void {
this.cells.value.forEach((cell, index) => insert(index, cell));
const unobserveInsert = this.onInsert.observe(({index, item}) => {
const unobserveInsert = this._onInsert.observe(({index, item}) => {
insert(index, item);
});
const unobserveRemove = this.onRemove.observe(index => {
const unobserveRemove = this._onRemove.observe(index => {
remove(index);
});
return () => {
Expand All @@ -210,26 +270,60 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
};
}

/**
* Find an item matching the predicate.
*
* @param predicate - A function to apply to each item.
* @returns The mutable cell of the first item matching the predicate or
* undefined if not found.
*/
find(predicate: (item: TItem, index: number) => boolean): MutCell<TItem> | undefined {
return this.cells.value.find((item, index) => predicate(item.value, index));
}

/**
* Insert an item.
*
* @param index - The index to insert the item at. Existing items will be
* moved over.
* @param item - The item to insert.
*/
insert(index: number, item: TItem): void {
const c = cell(item);
this.cells.update(cells => {
cells.splice(index, 0, c);
this.onInsert.emit({index, item: c});
this._onInsert.emit({index, item: c});
});
}

/**
* Get the current cell value at the given index.
*
* @param index - The index.
* @returns The item or undefined if out of bounds.
*/
get(index: number): TItem | undefined {
return this.cells.value[index]?.value;
}

/**
* Set the value of the cell at the given index.
*
* @param index - The index.
* @param item - The new cell value.
*/
set(index: number, item: TItem): void {
this.cells.value[index].value = item;
}

/**
* Update the value of the cell at the given index.
*
* @param index - The index.
* @param mutator - A function that modifies the value of the cell.
* @returns If the `mutator` function returns a value, that value is
* returned by `update` as well.
*/
update<T>(index: number, mutator: (value: TItem) => T): T | undefined {
return this.cells.value[index]?.update(mutator);
}
Expand Down Expand Up @@ -266,7 +360,7 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
const c = cell(item);
this.cells.update(cells => {
cells.push(c);
this.onInsert.emit({index, item: c});
this._onInsert.emit({index, item: c});
});
}

Expand All @@ -277,7 +371,7 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
remove(index: number): TItem | undefined {
if (index >= 0 && index < this.cells.value.length) {
const removed = this.cells.update(cells => cells.splice(index, 1)[0]);
this.onRemove.emit(index);
this._onRemove.emit(index);
return removed.value;
}
}
Expand All @@ -286,7 +380,7 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
for (let i = 0; i < this.cells.value.length; i++) {
if (predicate(this.cells.value[i].value, i)) {
this.cells.update(cells => cells.splice(i, 1))
this.onRemove.emit(i);
this._onRemove.emit(i);
i--;
}
}
Expand All @@ -304,9 +398,11 @@ export class CellArray<TItem> extends CellStream<TItem, void> {
*/
export class CellMap<TKey, TValue> extends CellStream<TValue, TKey> {
private readonly cells: MutCell<Map<TKey, MutCell<TValue>>> = cell(new Map(Array.from(this.initialEntries).map(([key, value]) => [key, cell(value)])));
private _onInsert = createEmitter<{key: TKey, value: Cell<TValue>}>();
private _onDelete = createEmitter<TKey>();
readonly size = this.cells.map(cells => cells.size);
readonly onInsert = createEmitter<{key: TKey, value: Cell<TValue>}>();
readonly onDelete = createEmitter<TKey>();
readonly onInsert = this._onInsert.asEmitter();
readonly onDelete = this._onDelete.asEmitter();

constructor(
private initialEntries: Iterable<[TKey, TValue]> = [],
Expand Down Expand Up @@ -336,12 +432,12 @@ export class CellMap<TKey, TValue> extends CellStream<TValue, TKey> {
keys.push(key);
insert(index, cell, key);
});
const unobserveInsert = this.onInsert.observe(({key, value}) => {
const unobserveInsert = this._onInsert.observe(({key, value}) => {
const index = keys.length;
keys.push(key);
insert(index, value, key);
});
const unobserveRemove = this.onDelete.observe(key => {
const unobserveRemove = this._onDelete.observe(key => {
const index = keys.indexOf(key);
if (index >= 0) {
keys.splice(index, 1);
Expand Down Expand Up @@ -369,21 +465,21 @@ export class CellMap<TKey, TValue> extends CellStream<TValue, TKey> {
} else {
const c = cell(value);
this.cells.update(cells => cells.set(key, c));
this.onInsert.emit({key, value: c});
this._onInsert.emit({key, value: c});
}
}

delete(key: TKey): boolean {
if (this.cells.update(cells => cells.delete(key))) {
this.onDelete.emit(key);
this._onDelete.emit(key);
return true;
}
return false;
}

clear(): void {
this.cells.update(cells => {
cells.forEach((_, key) => this.onDelete.emit(key));
cells.forEach((_, key) => this._onDelete.emit(key));
cells.clear();
});
}
Expand Down
34 changes: 34 additions & 0 deletions src/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,22 @@ export function Switch<
}

/**
* Dereference a nullable cell.
*
* @example
* ```tsx
* const num = ref<number>();
*
* <Deref ref={num}>{ num =>
* {num.map(n => n + 5)} is ten
* }</Deref>
* ```
*
* @param props.children - A function that accepts the non-nullable value and
* returns an element.
* @param props.ref - The cell to dereference.
* @param props.else - Optional element to render if the cell value is null or
* undefined.
* @category Components
*/
export function Deref<T>(props: {
Expand Down Expand Up @@ -577,6 +593,24 @@ export function Deref<T>(props: {
}

/**
* Unwrap a cell. The DOM is rebuilt whenever the input cell changes. If the
* cell value is null or undefined the `else`-branch will be rendered instead
* (similar to {@link Deref}).
*
* @example
* ```tsx
* const num = bind(5);
*
* <Unwrap from={num}>{ num =>
* {num + 5} is ten
* }</Unwrap>
* ```
*
* @param props.children - A function that accepts the unwrapped value and
* returns an element
* @param props.from - The cell to unwrap.
* @param props.else - Optional element to render if the cell value is null or
* undefined.
* @category Components
*/
export function Unwrap<T>(props: {
Expand Down
2 changes: 1 addition & 1 deletion src/emitter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export abstract class Emitter<TEvent> implements Observable<TEvent> {

/**
* Create an emitter from another observable (cell or emitter). The
* resulting emitter emits an even whenever the input observable emits and
* resulting emitter emits an even whenever the input observable emits an
* event.
*
* @param observable - The observable to wrap.
Expand Down
Loading

0 comments on commit 6d81778

Please sign in to comment.