A collection of TypeScript/JavaScript utilities designed to render simple or structured data collections (e.g.: Arrays of strings or Arrays of Objects) in the DOM and manage all sorts of updates in an efficient manner without using a Virtual DOM and minimal boilerplate.
With these you can create reactive collections and manage operations on them in either an imperative or functional style.
This library has reactive bindings exposed through the Observable/Observer interfaces (e.g.: RxJS) and integrates seamlessly with Observable UI libraries such as Rimmel.js which have first-class support for them.
ObservableTypes expose both the Observable and the Observer interfaces (like the Rx.Subject), which makes them suitable for piping, streaming and merging with other reactive event streams.
It also enables you to create data collections where changes are described as event streams.
You can also subscribe to any event stream to feed your collections.
Install via npm:
npm install observable-types
Then import it into your project:
import { Collection, CollectionSink } from 'observable-types';
A Collection
is a hybrid construct that implements the Array
, the Observable
and Observer
interfaces.
It behaves just like an Array, with all its standard methods such as push
, pop
, splice
, etc.
These will emit semantic notifications for rendering so they can use them to make changes in a UI in efficient ways without the need to perform any complicated or anyways heavy diffing work. E.G.: .push()
becomes an instruction like "append an item to the bottom of the view".
Additional semantic operations are also available, most notably .move(from, to)
, to enable efficient drag'n'drop operations by only specifying which item needs to move and where.
import { Collection } from 'observable-types';
// Construct an Item from a value
const Item = (value: string | number) =>
({ value, created: Date.now() });
// Create a simple Collection and pass the
// Item constructor for type integrity
const myList = Collection([1, 2, 3], Item);
// Create a stream of "unshift" operations.
// `data` is Observable, so it can be
// piped and subscribed to.
const insertions = data.observe('unshift');
// Do something when items are inserted
insertions.subscribe((e) => {
console.log(`Added`, ...e, ' to the top');
});
// Finally, modify the collection
myArray.unshift('new stuff');
// Logs: "Added {value: 'new stuff', ... } to the top"
myArray.push('other stuff');
// Logs nothing, as we're only listening for
// 'unshift' events
- assign(newItems: T[]): Replaces the entire array with a new array of items.
- move(src: number, dst: number, count: number): Moves a set of items from one index to another within the collection.
- observe(prop: string): Returns an Observable that emits changes for the specified operation (e.g., 'push', 'splice').
- toArray(): Returns the underlying array as a standard array.
- push(...items: T[]): Adds new items to the end of the collection and emits a reactive update.
CollectionSink
is an adapter used to render a Collection
into an HTML container and make efficient updates based on the semantic notifications emitted by the Collection
.
Since the Collection
notifications are semantic, they give enough information to render changes in the most efficient way without having to perform any diffing.
All you need is the collection and a template to use for rendering new items.
Observable Types best shine when used with an Observable-aware UI library such as Rimmel, in which connecting an observable stream to the DOM is as trivial as putting it in the template:
import { Collection, CollectionSink } from 'observable-types';
import { rml, Value } from 'rimmel';
const Item = (title) => ({ title });
const ItemTemplate = item => `<li>${item.title}</li>`;
const list = Collection([], Item);
const listView = CollectionSink(list, ItemTemplate);
document.body.innerHTML = rml`
<h1>List</h1>
<ul>
${listView}
</ul>
<input placeholder="new stuff" onchange="${Value(items)}">Add</button>
`;
It's not mandatory to use any UI library, though. In fact, you can also live without and imperatively sink an Observable Collection down the DOM by passing it a target node.
import type { ObservableItem } from 'observable-types';
import { Collection, CollectionSink } from 'observable-types';
interface I {
title: string;
timestamp: number;
}
const Item = (title: string): I => ({ title, timestamp: Date.now() });
const ItemTemplate = (item: ObservableItem<I>) => `<li>${item.title}</li>`;
const items = Collection(['Item 1', 'Item 2'].map(i=>Item(i)), Item);
const renderer = CollectionSink(items, ItemTemplate);
// Bind to a DOM element
items.subscribe(renderer.sink(document.querySelector('#app')));
// Make changes to see them rendered
setTimeout(() => items.push(Item('Item 3')), 1000);
setTimeout(() => items.move(0, 1), 2000);
setTimeout(() => items.pop(), 3000);
setTimeout(() => items.shift(), 4000);
Check out the following Kitchen Sink Application where you can play and experiment with the whole feature set
Contributions are welcome! Please feel free to open issues or submit pull requests.
Clone the repository.
Install dependencies:
# Copy code
npm install
npm test
vite
This project is licensed under the MIT License.