murx
TypeScript icon, indicating that this package has built-in type declarations

0.5.1 • Public • Published

μrx Join the chat at https://gitter.im/ZenyWay/murx

NPM build status coverage status Dependency Status

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 and untap 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

murx 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 tapped 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.

Package Sidebar

Install

npm i murx

Weekly Downloads

2

Version

0.5.1

License

SEE LICENSE IN LICENSE

Last publish

Collaborators

  • smcatala