A functional and reactive JavaScript framework for predictable code
MIT License
Bot releases are hidden (Show)
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.
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.
Published by staltz over 9 years ago
Minor improvement to add some console warnings for code smell use cases.
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 |
Published by staltz over 9 years ago
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:
Additional small feature:
You can listen to DOMUser errors on user.get('error$')
.
Published by staltz over 9 years ago
Fixes bug #87
Published by staltz over 9 years ago
Fix issue #89 with DOMUser and View
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'
Published by staltz over 9 years ago
Fixes issue #80.
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.
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.
Published by staltz over 9 years ago
Fix #66
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.
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.
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.
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.
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);
});
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.
Published by staltz over 9 years ago