Flux-view is a tiny library to render HTML documents using reactive programing primitives (tiny meaning less than 10kB uncompressed - rxjs not included). The library core concept is to allow binding DOM's attributes and children to RxJS streams in an HTML document:
import { interval } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { render, attr$ } from '@youwol/flux-view'
const nCount = 10
// timer$: tick 10 times every seconds
const timer$ = interval(1000).pipe(
take(nCount),
map( tick => nCount - tick)
)
let vDom = {
tag:'div',
innerText: 'count down:',
children:[
{ tag:'div',
innerText: attr$(
// input stream (aka domain stream)
timer$,
// rendering mapping
(countDown:number) => `Remaining: ${countDown} s`
)
}
]
}
let div : HTMLElement = render(vDom)
Few things to higlight:
For those having knowledge of RxJS and HTML, learning how to use the library will take a couple of minutes: the all API contains only 4 functions : render, attr$, child$, children$; the three latters are here essentially the same, they are differentiated as syntactic sugar. If not the case, learning how to use the library is learning reactive programming and HTML5.
More elaborated example are provided in codesandbox:
Demos sources are in the folder /src/demos (opening index.html in a browser will do the work).
Using npm:
npm install @youwol/flux-view
Using yarn:
yarn add @youwol/flux-view
And import the functions in your code:
import {attr$, child$, render} from "@youwol/flux-view"
Or you can start scratching an index.html using CDN ressources like that:
<html>
<head>
<script src="https://unpkg.com/rxjs@6/bundles/rxjs.umd.min.js">
</script>
<script src="https://unpkg.com/@youwol/flux-view@0.0.2/dist/@youwol/flux-view.js">
</script>
</head>
<body id="container">
<script>
let [flux, rxjs] = [window['@youwol/flux-view'], window['rxjs']]
let vDom = { innerText: flux.attr$( rxjs.of("Hi! Happy fluxing!"), (d)=>d) }
document.getElementById("container").appendChild(flux.render(vDom))
</script>
</body>
</html>
The virtual DOM (vDOM) is described by a JSON data-structure with following attributes (all are optionals):
Any of those attributes but 'tag', 'connectedCallback' and 'disconnectedCallback' can be:
To turn a vDOM into a regular HTMLElement, use the function render:
import { BehaviorSubject } from 'rxjs';
import { render } from '@youwol/flux-view'
let option$ = new BehaviorSubject<string>('option0')
let sub = option$.subscribe( option => {/*some behavior*/})
let vDom = {
class:'d-flex justify-content-center',
children:[
{
tag:'select',
children:[
{tag:'option', innerText:'option 1'},
{tag:'option', innerText:'option 2'},
],
onchange: (ev) => option$.next( ev.target.value)
}
],
connectedCallback: (elem) => {
/*This makes the subscription managed by the DOM, see part 'Lifecycle' */
elem.ownSubscription(sub)
}
}
let div = render(vDOom)
The functions attr$, child$ and children$ are actually the same, they differ only by the type used in their definition.
It follows this common type's definition (the third arguments is optional):
function (
stream$: Observable<TData>,
viewMap: (TData) => TResult,
{
untilFirst,
wrapper,
sideEffects
}:
{ untilFirst?: TResult,
wrapper?: (TResult) => TResult,
sideEffects?: (TData, HTMLElement) => void
} = {},
)
where:
let vDom = {
tag:'div', innerText: 'count down:',
children:[
{ tag:'div',
innerText: attr$(
timer$,
( countDown:number ) => `Remaining: ${countDown} s`,
{ untilFirst: "Waiting first count down..."}
)
}
]
}
let vDom = {
tag:'div', innerText: 'count down:',
children:[
{ tag:'div',
class: attr$(
timer$,
( countDown:number ) => countDown <5 ? 'text-red' : 'text-green',
{ wrapper: (class) => `count-down-item ${class}`}
),
innerText: attr$( timer$, (countDown:number) => `${countDown} s`)
}
]
}
Behind the scene, one central task of flux-view is to keep track of internal subscriptions and manage their lifecycle, without any concern for the consumer of the library.
The rule is straightforward: only the subscriptions related to DOM elements included in the document are kept alive. When an element is removed (in any ways), all the related streams are unsubscribed recursively. Latter on, if the element is reinserted in the document, all the related streams are resuscribed.
Most of the popular frameworks (e.g. React, Angular, Vue) use an approach that bind a state to a virtual dom and automagically identify and update relevant portions of the DOM that actually change when the state modification. This magic is at the price of a more complex API and at some undesired redrawing if care is not taken.
In flux-view, the user is in charge to chose how the binding between DOM's attributes/children and observables is realized. For instance, in the previous example, there is only the attribute innerText of the inner div that is actually updated: when timer$ emit a new value, only this property is updated. A less efficient implementation would be:
let vDom = {
tag:'div', innerText: 'count down:',
children:[
child$(
timer$,
(countDown:number) => ({ tag: 'div', innerText:`Remaining: ${countDown} s`})
)
]
}
In this case, the entire inner div is re-rendered when timer$ emit a new value.
There is yet one performance issue with flux-view that arises when a binding between an observable of a collection and the children of a node is desired using children$. At that time the library redraw the all collection, even if only one item has been added/removed/modified. This issue will be solved soon in upcoming versions (somehow by allowing more granularity when using *children$' to provide required features).
Until a better solution is found, coverage results are presented here
Generated using TypeDoc