Observables
Observables are a key concept to understand in order to be effective with Fuse. Observables let us automatically update the UI whenever there are changes to the data-model. They are also extremely useful for describing view-models; which means mapping the data-model into a view friendly format. When data-binding to an Observable
using the curly brace syntax ({<some_observable>}
), a subscription is automatically created that will update the UI whenever the Observable
changes. This article is a quick introduction to understanding and using the FuseJS/Observable API.
Here is a tiny example to give you an idea of what we'll be looking at:
<App>
<JavaScript>
var Observable = require("FuseJS/Observable");
module.exports.myValue = Observable(1);
</JavaScript>
<Text Value="{myValue}" />
</App>
Video tutorial
For a nice introduction to working with observables, take a look at this video:
<iframe width="560" height="315" src="https://www.youtube.com/embed/bB9P4mTGtVU" frameborder="0" allowfullscreen></iframe>
Using observables
An Observable
can hold a single value, or be treated as a list of values with 0 or more elements. When the value(s) of the Observable
changes, all subscribers are automatically notified. The concept of subscribers/subscriptions is discussed in detail later in this article.
Start off by importing the observable module, which returns a function:
var Observable = require("FuseJS/Observable");
Observables are created by calling the Observable
function directly with zero or more initial values.
var emptyObservable = Observable();
var singleValueObservable = Observable(true);
var listObservable = Observable(1,2,3,4);
var singleValueObservable = Observable(10); //now has .value == 10
singleValueObservable.value = 20; //now has .value == 20
var theValue = singleValueObservable.value; //theValue is now == 20
When using an observable as a list, we can add and remove items using the .add
and .remove
methods.
var multiValueObservable = Observable(1,2);
multiValueObservable.add(5); //now contains 1,2,5
multiValueObservable.remove(2); //now contains 1,5
We can also apply various transformations to observables using a set of methods we collectively call "reactive operators". These operators are methods we can call on observables that return new observables. We cover them in more detail later in this article. The following example squares all the numbers in the someNumbers
observable using the .map
operator. Map works by converting all the items in an Observable
from one form into an other using the supplied function.
var someNumbers = Observable(1,2,3,4); //now contains 1,2,3,4
var someNumbersSquared = someNumbers.map(function(x){ return x * x; }); //someNumbersSquared will eventually contain 1,4,9,16
Since all the reactive operators return new observables, we can put several of them after one another to create a long chain of reactive operations.
var someNumbers = Observable(1,2,3,4);
var someTransformedNumbers = someNumbers
.map(function(x){ return x * x; })
.where(function(x){ return x < 10; })
.map(function(x){ return -x; }); //will eventually contain -1, -4, -9
State Observables and derived Observables
When working with Observables, it is important to understand the difference between state Observables and derived Observables. A state Observable is an Observable that is explicitly made by you, while a derived Observable is returned by other APIs, such as reactive operators.
One thing to note about the example above, that catches a lot of new Fusers off guard, is that someTransformedNumbers
won't actually contain data immediately after the statement that declares it:
var someNumbers = Observable(1,2,3,4); // someNumbers is a state Observable
var someNumbersSquared = someNumbers.map(function(x){ return x * x; }); // someNumbersSquared is a derived Observable
console.log("SquaredNumbers length: " + someNumbersSquared.length); // this will print 0
The difference between state Observables and derived Observables is explained in Observable API docs. In short, derived Observables won't propagate data unless there is a subscriber at the end of the chain, while the data in state Observables is available synchronously. Keep reading to learn more about how subscriptions work.
Subscribing to changes in Observables
A really important thing to understand about Observables is that they need at least one subscriber before they start propagating data. There are a few ways to subscribe to an Observable:
Data-bind to it from UX Markup
Whenever we data-bind to an Observable
from UX using the curly brace syntax, we automatically create a subscription to it. This is the most common way of creating subscriptions. For more information about data-binding, take a look at this article.
.onValueChanged(module, func(item))
In some cases, we're interested in running some imperative code in response to an Observable
changing. We can do this by subscribing using the .onValueChanged(module, func(item))
function, which fires the function func
every time the value of a single value Observable changes. The module parameter lets us connect the lifetime of this subscription to the lifetime of a module. In most cases we just pass the module we're currently in:
var myObservable = Observable(1);
myObservable.onValueChanged(module, function(item) {
//do something
});
.addSubscriber(func)
We can also explicitly add subscriptions by using the .addSubscriber(func(item))
method. The drawback of using .addSubscriber
is that we manually have to remove the subscription when we no longer need it using .removeSubscriber
. Always prefer using .onValueChanged
unless we're defining custom reactive operators. We mention it here only for completeness.
.subscribe(module)
Lastly, there are rare cases when we need to force an Observable to be calculated without data-binding to it from UX Markup. .subscribe(module)
is similar to .onValueChanged(module, func(item))
in that it adds a subscription which is tied to the lifetime of a module. Note that it only creates this subscription and does not accept a function as second argument.
Reactive operators
Reactive operators are methods we can call on observables that produce new observables. These can then be subscribed to or transformed further by additional applications of reactive operators. An important thing to know about these methods, is that they work declaratively. Applying a reactive operator to an Observable
doesn't actually apply the transformation right away; it merely sets up a "pipeline" that the values will flow through when they are supplied at the source Observable
.
We say that observables uses a "push based" model. The alternative to a push based model is one that is "pull based". In this scenario, the UI will have to ask its data-source for the latest data when it wants to update itself. In the push based approach, the data-model will push its values towards the UI whenever they change.
A thing to be aware of with this "push based" approach, is that values won't actually be pushed through unless someone has subscribed to be notified of these pushes, as mentioned earlier.
What do we mean by an Observable producing values
We usually think of observables as streams of values flowing from a source (or a set of sources) to a destination (or set of destinations). Along the way, the stream can be transformed, filtered and combined with other streams by using reactive operators like .map
, .where
and .combine
(find information about all the operators here. Here is an example:
var source = Observable(2);
var destination = source.map(function(x) {
return x * x;
}).where(function(x) {
return x < 50;
});
In the code above, we create a source Observable
with a single initial value; the number 2. We then use the reactive operator .map
to transform the value with a "mapping-function" that squares the number. Lastly, we use the reactive operator .where
to filter the observable so that only values that are below 50 will be propagated further. The .where
operator is expected to return either true
or false
; true
if we allow the value to propagate, false
if not.
Passing observables through properties
Observables can also be passed into custom made components using Properties. We can add a property which accepts Observables by making an object
property:
<Panel ux:Class="CoolPanel">
<object ux:Property="ObservableProperty" />
<JavaScript>
var passedInObservable = this.ObservableProperty.inner();
</JavaScript>
</Panel>
<JavaScript>
var Observable = require("FuseJS/Observable");
module.exports = {
valueToPass: Observable("123")
};
</JavaScript>
<CoolPanel ObservableProperty="{valueToPass}" />
In most cases you'll want to use inner()
when fetching an Observable passed in through properties. This is because the javascript value this.Propertyname
is an observable with whatever Propertyname
contains. If we pass an Observable in, this.Propertyname
will contain an observable with the observable we passed through.