murx
v0.5.1
Published
micro reactive UI framework
Downloads
2
Maintainers
Readme
μrx
micro reactive UI framework:
- ideal for mobile and snappy apps: 61 lines of ES5 code, 1322 bytes uglified, 609 bytes compressed.
- easy to code:
- exports only one tiny factory that instantiates a μrx pipe.
- a μrx pipe is merely a reducer of immutable model instances, with:
- an Observer that sinks functions for processing the model in the reducer,
- a
start
method that returns an Observable of model instances from the reducer, - and
tap
anduntap
methods to create child sinks for child components.
- async at its core, rxjs-based,
- all plain old ES5 javascript, including typescript type definitions.
- easy to learn:
- concise documentation = this short README, including two online examples.
- if you've read this far, you already know (most of) all there is to know!
- easy to test and debug:
- app state fully captured in immutable model snapshots,
- unidirectional flow around a single app-wide reducer,
- clear separation of concerns, self-contained components that
require
their dependencies, - mock everything around a component: other components, rendering, side-effects...
together with the equally tiny but powerful yo-yo HTMLElement rendering library, RxJS, and any solid HTML framework such as Bootstrap or PureCss, rapidly build beautiful, powerful reactive web apps composed of loosely coupled, self-contained, single-purpose components.
although the examples in this document use yo-yo
to render HTMLElements,
μrx is completely agnostic to the rendering engine.
in fact, μrx is compatible with any type of rendering library,
e.g. that render to log files, to a test mock, to a node stream, a websocket,
or even papyrus...
simple example: async counter
import newMurxPipe, { AsyncDiff, Renderer } from 'murx'
import { Observable, Observer } from 'rxjs'
const yo = require('yo-yo')
import debug = require('debug')
debug.enable('murx:*')
interface CounterModel {
value: number
disabled?: boolean
}
// create a new murx pipe
const murx = newMurxPipe<CounterModel>() // { diff$i, start, tap }, tap method not used in this example
const render: Functor<CounterModel,HTMLElement> =
({ value, disabled }: CounterModel) => yo
`<div class="col-sm-6">
<div class="card text-center">
<div class="card-block">
<h3 class="card-title">slow async counter</h3>
<p class="card-text">${value}</p>
<button type="button" class="btn btn-primary"
${disabled ? 'disabled' : ''} onclick=${onclick}>
<i class="fa ${disabled ? 'fa-spinner fa-spin' : 'fa-plus'}"></i> ${disabled ? 'processing...' : 'increment' }
</button>
</div>
</div>
</div>`
// apply the increment functor to the model on every click
const onclick = () => murx.diff$i.next(slowAsyncIncrement)
// AsyncDiff functors emit a sequence of states (model snapshots) based on a given state
const slowAsyncIncrement: AsyncDiff<CounterModel> = (model: CounterModel) =>
Observable.of({ disabled: false, value: model.value + 1 }) // increment counter...
.delay(1000) // ...after an imaginary slow async operation...
.startWith({ value: model.value, disabled: true }) // meanwhile, disable the button
const container = document.getElementById('murx-example-app')
// start the murx reducer !
const init = { value: 0 }
murx.start(init).do<CounterModel>(debug('murx:model:'))
.map(model => render(model)) // render the model
.scan((target, source) => yo.update(target, source)) // update the target element
.distinctUntilChanged() // yo-yo may occasionally return a new target element... (although only once in this example)
.forEach(counter => container.appendChild(counter)) // update the DOM for every new target element (again, only once here)
.catch(debug('murx:error:'))
the files of this example are available in this repository.
view a live version of this example in your browser console, or clone this repository and run the following commands from a terminal:
npm install
npm run example:simple
proposed app architecture
the above diagram illustrates how a murx-based app may be architectured. note that murx is completely agnostic to rendering: its API is limited to sinking async functors that are applied to model instances in the reducer, the output of which is available as a source of model instances.
in the proposed architecture, which is by no means imposed, the app component is composed of a murx pipe and its rendering function. likewise, child components are composed of a murx sink tapped off that of its parent component and of their rendering function.
parent components define wrap
and unwrap
functions
that respectively map child to parent model instances and vice-versa.
these functions are required to tap a child diff sink off the parent's sink:
when the child diff is applied, the parent model is first mapped
to its child scope. after applying the diff to the resulting child model,
the result is mapped back to the parent scope.
the unwrap
functions may also be called by the rendering function
before calling the corresponding child rendering function.
this architecture ensures that each component is provided with a corresponding scoped view of the model stream, defined by its parent component. components are hence fully self-contained and may be composed as desired.
diff$i async functor sink stream
the architecture diagram introduces the diff$i
async functor sink stream:
functors fed into the $diffi
sink are applied
to the current state (model snapshot).
async functors are simply functions that map the current state
to an Observable sequence (stream) of states.
under the hood, the stream of async functors is input to a state reducer that simply applies each functor to the current state, and merges the output Observable sequence into the output state sequence.
in the above example, although slowAsyncIncrement
is a pure function
(it has no side-effects), it still demonstrates how simple it is
to work with asynchronous processes.
in fact, slowAsyncIncrement
could easily be replaced
by an asynchronous impure function:
because functors process state and return an Observable sequence of states,
they are ideal for triggering model-based side-effects,
in particular asynchronous side-effects,
e.g. to fetch data from an end-point into the model,
or store data from the model in a database,
or anything else really.
example with multiple components
the simple example above is limited to rendering a single component.
wiring up an app with multiple components is just as easy.
components are just simple modules:
they require
and manage their own component dependencies,
as illustrated by the (nearly) self-explanatory todo
example,
in which the main application component requires a todo-card component.
the files of the todo
example are available in this repository.
view a live version of this example in your browser console, or clone this repository and run the following commands from a terminal:
npm install
npm run example:todo
the todo
example demonstrates one way of wiring an application.
however, the μrx API does not impose this choice.
here, we choose to export a factory that instantiates a rendering function.
the factory itself inputs a MurxSink
instance and a map of event handlers.
handlers may be used to efficiently 'bubble-up' events from a child-component to a parent up its hierarchy that knows what to do with it.
as for the MurxSink
instance, it is obtained from the tap
method
of the main application's MurxSink
instance:
the diff$i
Observer of the returned MurxSink
instance
is tap
ped off the diff$i
Observer of the main application's MurxSink
.
the tap
method takes two function arguments:
- the first,
unwrap
maps a parent instance to a new child model instance, - the second,
wrap
maps a child model instance into a new parent instance.
under the hood,
the diff$i
Observer returned by the tap
method
injects wrapped diff
async functors into the parent diff$i
observer.
wrapped diff$i
functors are hence applied to the model reducer.
before applying the diff
, the parent model is unwrapped into a child model.
after applying the diff
and before injecting the result into the reducer,
the diff
result is wrapped back into a new instance of the parent model.
an application hence only runs on a single state reducer instantiated by the topmost component, and each component may access its own scope within the global app state, and only its scope. μrx makes no assumptions on how a child is mapped from a parent, or vice-versa, and leaves full freedom to how scopes are defined.
events from a child component that should affect
the model outside of its scope are simply bubbled up and handled by
the appropriate parent component,
as illustrated in the todo
example with the ondelete
event.
note however, that state parameters should not be bubbled-up through handlers.
instead, if a child component requires partial access to a parent's scope,
the parent should enable such access through the unwrap
and wrap
functions.
use of handlers should be restricted to events that must be handled further up
the hierarchy. when processed, these events might, or might not result
in a modification of state.
note that the tap
method subscribes the parent diff$i
Observer
to that of the returned child instance.
the subscription may be released with the latter's untap
method,
after which the child instance may be disregarded.
this should be done by the child instance's parent instance,
that instantiated the former, and hence manages its life cycle,
as illustrated in the example.
API v0.5 experimental
ES5
and Typescript
compatible.
coded in Typescript 2
, transpiled to ES5
.
type MurxPipeFactory = <M>() => MurxPipe<M>
interface MurxPipe<M> extends MurxSink<M> {
start (value: M): Observable<M>
}
interface MurxSink<M> {
diff$i: Observer<AsyncFunctor<M,M>>
tap <S>(unwrap: Functor<M,S>, wrap: (parent: M, child: S) => M): MurxSink<S>
untap (): void // release the internal subscription of the parent Observer to that of this instance, if any.
}
type AsyncFunctor<T,U> = Functor<T,Observable<U>>
type Functor<T,U> = (val: T) => U
the following generic types are provided for convenience, when working with any type of renderer, but are not required by murx:
type ComponentFactory<M, H extends Handlers, S, C> =
(sink: MurxSink<M>, handlers?: Partial<H>, opts?: Partial<S>) => C
interface Handlers {
[ key:string ]: (...args: any[]) => void
}
that's it... go murx your app!
~~for a detailed specification of the API, run the unit tests in your browser.~~
CONTRIBUTING
see the contribution guidelines
LICENSE
Copyright 2017 Stéphane M. Catala
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and Limitations under the License.