cyclejs

A functional and reactive JavaScript framework for predictable code

MIT License

Downloads
222.9K
Stars
10.2K
Committers
134

Bot releases are hidden (Show)

cyclejs - v0.20 - Streams: a dramatic breaking change

Published by staltz over 9 years ago

v0.20 is unfortunately or fortunately a "this changes everything" version. We removed: DataFlowNode, DataFlowSink, DataFlowSource, Model, View, Intent, DOMUser. New API is just: render(), createStream(), and the familiar registerCustomElement().

Streams are a core part of Cycle now, and everything you need to know about them are explained here.

The rationale is: where DataFlowNode supported multiple output Observables, a Stream supports only one output Observable because a Stream is an Observable. The benefit with this new API is the removal of getters, and a rather simpler abstraction since you only need to think of how to compose Streams.

A single-output DataFlowNode is replaced by a Stream.
A single-input DataFlowSink is replaced by just a subscriber to an Observable.
A single-output DataFlowSource is replaced by a normal Rx.Observable.

BEFORE

let FooNode = Cycle.createDataFlowNode(function (BarNode) {
  return {
    foo$: BarNode.get('bar$').map(bar => 'This is bar: ' + bar)
  };
});

AFTER

let foo$ = Cycle.createStream(function (bar$) {
  return bar$.map(bar => 'This is bar: ' + bar);
});

The DOMUser was a DataFlowNode, hence it was "function-like". Now the user is just a normal JS function implemented by the developer. Cycle as a framework provides render(vtree$, container), which just outputs rootElem$, an Observable emitting the DOM element rendered from vtree$ into the given container. This rootElem$ is special in that it contains a reference to another Observable: rootElem$.interaction$, representing all possible events from all children elements under the root element. You filter on interaction$ by calling choose(selector, eventName). This is how a user is represented in Cycle v0.20:

let interaction$ = Cycle.createStream(function user(vtree$) {
  return Cycle.render(vtree$, '.js-container').interaction$;
});

Custom elements are still registered globally with registerCustomElement(tagName, definitionFn), but the definitionFn is expected to have a different signature. definitionFn(rootElem$, props) is the signature, where rootElem$ is a Cycle Stream which should be injected a vtree$ that you should create inside the definitionFn. Properties are accessible using their dollar name on the props object. Unfortunately you must use a getter with the props object: props.get('color$'), while ES6 proxies are still unfeasible. See this example of a custom element.

Read the (short!) API documentation, check out examples and open issues or join the gitter channel for questions and feedback.

cyclejs - v0.18.2 - Custom elements can have nested vtrees

Published by staltz over 9 years ago

Implements #97. This is small yet nice addition to Custom elements. You can now provide children vtrees to a custom element as such:

h('my-element', [
  h('h3', 'Hello World')
])

You access these in the custom element's implementation like this:

Cycle.registerCustomElement('my-element', function (user, props) {
  var view = Cycle.createView(function (props) {
    return {
      vtree$: props.get('children$').map(children =>
        h('div.wrapper', children)
      )
    };
  });

  // ...
})

Check the example here.

cyclejs - v0.18.1 - Adds friendly warnings

Published by staltz over 9 years ago

Minor improvement to add some console warnings for code smell use cases.

cyclejs - v0.18.0 - Small breaking change on DataFlowSink, plus improvements

Published by staltz over 9 years ago

The only breaking change in this version is very small: DataFlowSink.inject() now returns the input it was given, instead of returning the Rx.Disposable that the definitionFn of DataFlowSink returns. This makes DataFlowSink consistent with DataFlowNode and DataFlowSource.

BEFORE

sink.inject(node1).inject(node2); // THROWS ERROR because sink.inject(node1) returns Rx.Disposable

AFTER

sink.inject(node1).inject(node2); // works fine, because sink.inject(node1) returns node1

dispose() function on DataFlowNode and DataFlowSink. These classes contain Rx.Disposable internally, and in the rare use case that you need to dispose them, this function allows you to do that. Normally you don't need to do this since DataFlowNodes are created once on application bootstrap and intended to live forever during the application run. Use dispose() in case you are dynamically creating and destroying DataFlowNodes.


get() function added to DataFlowSource. It was missing and should exist to stay consistent with DataFlowNode.


These are the functions that each type contains.

| contains? | DataFlowSource | DataFlowNode | DataFlowSink |
|-----------|----------------|--------------|--------------|
| get()     | YES            | YES          | no           |
| inject()  | no             | YES          | YES          |
| dispose() | no             | YES          | YES          |
cyclejs - v0.17.1 - Update RxJS to 2.4.7

Published by staltz over 9 years ago

cyclejs - v0.17.0 - View rejects custom element as root

Published by staltz over 9 years ago

This version changes the contract in Cycle so that the DOMUser throws an error if it is injected a View that has a Cycle custom element at the root.

THIS IS NOW REJECTED:

let View = Cycle.createView(Model => ({
  vtree$: Model.get("active$").map(active =>
    h('mycustomelement', {active: active}) // <== here
  }),
}));

YOU NEED TO WRAP IT:

let View = Cycle.createView(Model => ({
  vtree$: Model.get("active$").map(active =>
    h('div', [ // <== here
      h('mycustomelement', {active: active})
    ])
  }),
}));

The rationale behind this is two-fold:

  1. This isn't a priority as a feature at the moment. Custom element at the root was considered a bug (#89), and it was partially fixed. More problems related to this recently appeared, and the solution was getting complicated since it's deep into virtual-dom's diff and patch, Widgets, custom elements, etc. I prefer to explicitly not support this use case, then to either have it buggy or to spend a lot of time fixing it. The good news is that later on (maybe post-1.0) it is easy to add support for custom element at the root, while staying backwards compatible (you will also be able to keep on using the div wrapper). In other words, rather do a breaking change now.
  2. A custom element alone in the View is a code smell. If you only have the custom element, then why do you need the View wrapping the custom element? I currently can't see how custom element as the single element in a View is a meaningful pattern. But anyway, if someone discovers this is genuinely important use case, then read argument (1) above where we can add in the future support for this without introducing a breaking change.

Additional small feature:
You can listen to DOMUser errors on user.get('error$').

cyclejs - v0.16.3 - Fix custom events from custom elements

Published by staltz over 9 years ago

Fixes bug #87

cyclejs - v0.16.2 - Fix issue #89 with DOMUser and View

Published by staltz over 9 years ago

Fix issue #89 with DOMUser and View

cyclejs - v0.16.0 - Breaking change to remove clone()

Published by staltz over 9 years ago

clone() removed from all DataFlowNodes, including Model, View, Intent, and DOMUser. This is in order to reduce API surface, and clone() isn't essential, and is easily replaceable.

BEFORE

var node = Cycle.createDataFlowNode(function (input) {
  return {out$: input.delay(100)};
});
var cloned = node.clone();

AFTER

function definitionFn(input) {
  return {out$: input.delay(100)};
}
var node = Cycle.createDataFlowNode(definitionFn);
var cloned = Cycle.createDataFlowNode(definitionFn);

v0.16 also adds a type property to each DataFlowNode and other entities. For instance

var node = Cycle.createDataFlowNode(function (input) {
  return {out$: input.delay(100)};
});
console.log(node.type); // 'DataFlowNode'
var view = Cycle.createView(function (input) {
  return {vtree$: input.delay(100)};
});
console.log(view.type); // 'View'
cyclejs - v0.15.3 - Fix Cycle's importability as an npm package

Published by staltz over 9 years ago

Fixes issue #80.

cyclejs - v0.15.1 - Harmless updates

Published by staltz over 9 years ago

Updated RxJS to 2.4.1, which promises better performance for Observables. And updated virtual-dom to 2.0.1.

cyclejs - v0.15.0 - Small breaking change to custom elements

Published by staltz over 9 years ago

Very small breaking change to the API to use custom elements, but nevertheless a breaking change. This version fixes custom element glitches such as the ones described in #77. Most of your v0.14 code will work in v0.15, but we advise to use keys on custom elements as much as possible.

BEFORE

h('my-customelem', {foo: 'all your base are belong to us'})

AFTER

h('my-customelem', {key: 1, foo: 'all your base are belong to us'})

Notice the key for the custom element. This is a virtual-dom key attribute for virtual-dom Widgets.

This breaking change and advice also makes a lot of sense for rendering as a whole, this is not a leak in the abstraction. It is necessary for identifying that the element in question is still the same even though its properties might have changed. Counterexample:

Imagine you have a custom element 'gif-with-filters' which plays an animated gif, but you can specify a color filter such as "black and white" or "sepia". If you don't use a key for this element, when the filter property changes (for instance from BW to sepia), we will have an ambiguous situation. The virtual DOM diff and patch will not know whether you want to (1) replace the current gif-with-filters BW with a new gif-with-filters sepia, hence restarting the gif animation, or (2) keep the same gif-with-filters element but just swap the filter from BW to sepia without restarting the animation. To fix this ambiguity we use keys. If you want (1), then you provide a different key when you change the filter. If you want (2), then you provide the same key but change the filter property. Both cases (1) and (2) should be equally easy to express, and should be a responsibility of the app developer.

cyclejs - v0.14.4 - Fix glitch with lists of custom elements

Published by staltz over 9 years ago

Fix #66

cyclejs - v0.14.3 - Important consistency fixes to custom elements

Published by staltz over 9 years ago

Fixes glitches with custom events. Fixes behavior of custom elements that have a non-div element at the View's root.

cyclejs - v0.14.2 - Fix corner case bug for custom elements

Published by staltz over 9 years ago

Fix dispatching of custom events from custom elements

Particularly, for custom elements whose View.vtree$ has a non-div element on the root.

cyclejs - v0.14.1 - Fix a bug in DOMUser classnames

Published by staltz over 9 years ago

A necessary fix when using custom elements like this h('my-element.some-class') so it can be used like this User.event$('.some-class', 'someevent') no matter if '.some-class' refers to a custom element or a standard element.

cyclejs - v0.14.0 - Fix a bug with User.event$()

Published by staltz over 9 years ago

User.event$(selector, eventName) was not working for some custom elements. This fix supports only a selector which is a single class name, e.g., .foo. Using simple and single class names for elements in your Views is also a best practice for development.

cyclejs - v0.13.0 Small breaking change to custom elements

Published by staltz over 9 years ago

When registering a custom element, the definitionFn used to expect two parameters: element and Properties. With this breaking change, element is replaced with User.

The purpose is to prepare the API for the upcoming HTMLRenderer for server-side rendering, and also for reducing boilerplate, since any custom element implementation had to define a User in the same exact way: var User = Cycle.createDOMUser(element);.

Migration guide:
BEFORE

Cycle.registerCustomElement('my-element', function (element, Properties) {
  var View = Cycle.createView(function (Properties) {
    return {vtree$: Properties.get('foo$').map((x) => h('h3', String(x)))};
  });
  var User = Cycle.createDOMUser(element); // notice this
  User.inject(View);
});

AFTER

Cycle.registerCustomElement('my-element', function (User, Properties) {
  var View = Cycle.createView(function (Properties) {
    return {vtree$: Properties.get('foo$').map((x) => h('h3', String(x)))};
  });
  User.inject(View);
});
cyclejs - v0.12.1 - Revolutionary breaking change in Views

Published by staltz over 9 years ago

Replace Renderer with DOMUser, allowing Views to be event agnostic.

This breaking change version implements #73. From now on, instead of using onclick: 'click$' in properties in Views, you instead select which events you are interested in, with User.event$('.myelement', 'click') inside Intents.

This also brings an interesting twist to the overall MVI architecture. It's now MVUI: Model-View-User-Intent. There is no more Renderer (a DataFlowSink), now we have DOMUser (a DataFlowNode). This means that the DOMUser acts as a function: takes the View's vtree$ as input, renders that to the DOM as a side-effect, and outputs DOM events. You can select which DOM event stream by calling User.event$(selector, eventName), and of course, the output is an RxJS Observable.

Migration guide:
BEFORE

var HelloView = Cycle.createView(Model =>
  ({
    vtree$: Model.get('name$').map(name =>
      h('div', [
        h('label', 'Name:'),
        h('input', {
          attributes: {'type': 'text'},
          oninput: 'inputText$'
        }),
        h('h1', 'Hello ' + name)
      ])
    )
  })
);

var HelloIntent = Cycle.createIntent(View =>
  ({changeName$: View.get('inputText$').map(ev => ev.target.value)})
);

AFTER

var View = Cycle.createView(Model =>
  ({
    vtree$: Model.get('name$').map(name =>
      h('div', [
        h('label', 'Name:'),
        h('input.field', {attributes: {type: 'text'}}),
        // notice no more oninput here
        h('h1', 'Hello ' + name)
      ])
    )
  })
);

// Notice this, replaces Renderer, but has basically the same API
var User = Cycle.createDOMUser('.js-container');

// Notice Intent from now on should always take a DOMUser as input, NOT a View
var Intent = Cycle.createIntent(User =>
  // Notice the usage of User.event$()
  ({changeName$: User.event$('.field', 'input').map(ev => ev.target.value)})
);

And since DOMUser is now a DataFlowNode in the middle of the cycle, the injection part becomes:

User.inject(View).inject(Model).inject(Intent).inject(User);

Custom elements

Are now slightly simpler to define. No need for a View as an implementation, and it doesn't need to return vtree$ anymore since it will have it's own internal DOMUser.

             // function, not createView. And notice element here
var HelloComponent = function(element, Properties) {
  var HelloModel = Cycle.createModel((Properties, Intent) =>
    ({
      name$: Properties.get('name$')
        .merge(Intent.get('changeName$'))
        .startWith('')
    })
  );

  var HelloView = Cycle.createView(Model =>
    ({
      vtree$: Model.get('name$')
        .map(name =>
          h('div', [
            h('label', 'Name:'),
            h('input.myinput', {attributes: {type: 'text'}}),
            h('h1', 'Hello ' + name)
          ])
        )
    })
  );

  // Notice DOMUser internal here. It should use the element from above
  var HelloUser = Cycle.createDomUser(element);

  var HelloIntent = Cycle.createIntent(User =>
    ({changeName$: User.events('.myinput', 'click').map(ev => ev.target.value)})
  );

  HelloIntent
  .inject(HelloUser)
  .inject(HelloView)
  .inject(HelloModel)
  .inject(Properties, HelloIntent);

  return {
    // notice no more vtree$ exported here
    // this part is only used for custom events exported out of this component
  };
};

Cycle.registerCustomElement('hello', HelloComponent);

Basically, v0.12 is the user as a function in your UI architecture, and the View focusing only on what it does best: rendering.

cyclejs - v0.11.1 - Allows Renderer to target DocumentFragment

Published by staltz over 9 years ago

  • It was previously impossible to use Cycle.js with custom elements and Shadow DOM, because shadowRoot is DocumentFragment, not HTMLElement. This is now fixed, thanks to a PR from @erykpiast.