murx v0.5.1
μ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
startmethod that returns an Observable of model instances from the reducer, - and
tapanduntapmethods 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
requiretheir 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:simpleproposed 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](./src/index.ts#L70-L72), 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:todothe 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 tapped off the diff$i Observer of the main application's MurxSink.
the tap method takes two function arguments:
- the first,
unwrapmaps a parent instance to a new child model instance, - the second,
wrapmaps 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) => Uthe 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.0Unless 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.
