github cyclejs/cyclejs v0.23.0
Drivers - new possibilities, breaking changes, but no breaking news

latest releases: unified-tag, v7.0.0, v7.0.0-rc8...
pre-release8 years ago

v0.23 introduces Drivers: a plugin-like API which allows you to build a conversation between your Cycle.js app and any side-effectful target or targets.

Previously, Cycle's abstraction was a circular interaction between the app and the user on the DOM. The core function applyToDOM() meant that all Cycle apps were tied to the DOM. Drivers generalize this idea and allow you to target not only the DOM, but any other interactive target, such as a server with WebSocket, iOS renderers, etc.

Before, the user() function was embedded inside applyToDOM(). With drivers, you create the user() function and provide it to Cycle.run(). Compare these:

BEFORE

Cycle.applyToDOM('.js-container', appFn);

AFTER

var userFn = Cycle.makeDOMDriver('.js-container');

Cycle.run(appFn, { DOM: userFn });

Cycle comes with makeDOMDriver(), a factory function which generates a "DOM user" function. Cycle.run(appFn, drivers) circularly connects appFn with a group of driver functions specified by drivers object.

The advantage of this is you can now externalize side effects from the app function. Potential drivers could be "XHR Driver", "WebSocket Driver", "Canvas Driver", etc. Each driver is just a function taking an Observable as input and producing an Observable as output (or a collection of Observables).

This is last major API development before v1.0.


Migration guide

Drivers imply some breaking change, but only with small boilerplate at some interfaces. This isn't as radical breaking change as, for instance, v0.21.

If your code is built with MVI architecture, this migration should only affect some boilerplate on Views and Intents. Models should stay intact.

The contract of your appFn() or main() has changed: it should take one single "queryable collection of Observables" as input and output one "collection of Observables".

BEFORE

function computer(interactions) {
  return interactions.get(selector, type).flatMap( /* to vtree observable */ );
}

AFTER

function main(drivers) {
  var vtree$ = drivers.get('DOM', selector, type).flatMap( /* to vtree observable */ );
  return {
    DOM: vtree$,
    driverFoo: // some observable...
    driverBar: // another observable...
  };
}

The drivers argument is a queryable collection of Observables. drivers.get(driverName, ...params) will get an Observable returned by the driver named driverName, given ...params. Each driver can have its separate API for ...params. For instance, for the DOM driver, the API is drivers.get('DOM', selector, eventType).

The returned object should have Observables, and the keys should match the driver name. You give the driver name to the run() function:

Cycle.run(main, {
  DOM: Cycle.makeDOMDriver('.js-container'),
  driverFoo: // a driver function for Foo
  driverBar: // a driver function for Bar
});

Migration steps:

  1. Replace interactions.get(...params) with drivers.get('DOM', ...params)
  2. Replace the return of the app function from return vtree$ to return {DOM: vtree$}
  3. The dollar sign convention is no longer required by any Cycle.js feature. It is still recommended for applications, but not enforced by the framework, ever.
  4. renderAsHTML() became a driver of its own: makeHTMLDriver(). Use the HTML Driver like any other driver. See the isomorphic example for more details.

Custom elements

Registering custom elements is not anymore a global mutation function. Instead, when building the DOM Driver function with Cycle.makeDOMDriver(), you provide an object where keys are the custom element tag names, and values are the definition functions for those custom elements.

EXAMPLE

Cycle.run(app, {
  DOM: Cycle.makeDOMDriver('.js-container', {
    'my-element': myElementFn
  })
});

The definition function contract has changed slightly, but follows the same idea of the main() function contract: takes a queryable collection of Observables, and should output a collection (object) of Observables.

BEFORE

function myComponent(interactions, props) {
  var destroy$ = interactions.get('.remove-btn', 'click');
  var id$ = props.get('itemid');
  // ...
  return {
    vtree$: vtree$,
    destroy$: destroy$,
    changeColor$: changeColor$,
    changeWidth$: changeWidth$
  };
}

AFTER

function myComponent(drivers) {
  var destroy$ = drivers.get('DOM', '.remove-btn', 'click');
  var id$ = drivers.get('props', 'itemid');
  // ...
  return {
    DOM: vtree$,
    events: {
      destroy: destroy$,
      changeColor: changeColor$,
      changeWidth: changeWidth$
    }
  };
}

Migration steps:

  1. Replace function signature from function (interaction, props) to function (drivers)
  2. Replace interactions.get(...params) with drivers.get('DOM', ...params)
  3. Replace props.get(...params) with drivers.get('props', ...params)
  4. Replace the return of the definition function from return {vtree$, myEvent$, ...} to return {DOM: vtree$, events: ...}, where events: ... is an object where keys are the event name (notice no more dollar sign $ convention!) and values are Observables.

Don't miss a new cyclejs release

NewReleases is sending notifications on new releases.