This repo will contain documentation, implementation, tests and examples for a new ViewModel API that we propose to add to a future version of Fuse if it turns out well.
This is an open source effort, pull requests are welcome.
The ViewModel
API is a plain JavaScript wrapper on top of the Observable
API that provides a familiar looking component infrastructure
to people coming from Vue, Angular, React and similar frameworks. Instead of having the user to juggle raw observables, with all their
unfamiliar oddities, ViewModel
wraps a components internals in a well defined structure where there is less room for errors
and improvisation on a component-by-component basis.
- Plain Observables are somewhat hard to teach, hard to learn and hard to debug. In particular, newbies are often confused by the asynchronous nature of observables and the fact that no data flows unles they are subscirbed to.
- Many JS developers are more familiar with a stricter component model (Vue, React, Angular).
- The current (0.27) vanilla Fuse pattern encourages non-strict view model code, where
this
has a defined meaning in the root scope and non-standard symbols are injected. ThisViewModel
class intends to wraps that up so we get complete strict mode. ViewModel
can be implemented as a plain JS layer that results in a tree of Observables, requiring no new protocol between JS and UX.
ViewModel
objects are created by calling the ViewModel
function:
var ViewModel = require("FuseJS/ViewModel");
var vm = ViewModel(module, { /* descriptor */ });
A ViewModel
is passed the current module
to manage lifetimes of event listeners and observable subscriptions automatically. It also
gives access to the root object this
and other symbols injected, enables strict mode access to these symbols.
In a common and recommended UX markup scenario, each ux:Class
has its own ViewModel
in <JavaScript>
, which makes up the entire module.exports
.
<Page ux:Class="TodoPage">
<JavaScript>
var ViewModel = require("FuseJS/ViewModel");
module.exports = ViewModel(module, {
states: {
tasks: [],
newTask: ""
},
computed: {
tasksRemainingLabel: function (){
var count = this.tasks.filter(function(x) { return !x.isDone; }).length;
return "There are " + count + " remaining tasks."
}
},
methods: {
addTask: function() {
tasks.add({ label: this.newTask, isDone: false });
this.newTask = "";
}
},
onchanged: {
Parameter: function(p) {
console.log("The parameter to this page changed to " + JSON.stringify(p));
}
}
}
</JavaScript>
<Each Items="{tasks}">
<StackPanel Orientation="Horizontal">
<Text Value="{label}" />
<Switch Value="{isDone}" />
</StackPanel>
</Each>
</Page>
The states
section of the descriptor holds plain data variables that may change over the lifetime of the component.
states: {
tasks: [],
newTask: ""
}
The states
should only be modified by methods
and events
. The ViewModel
will create a hidden Observable
and expose
a property on the context object.
The computed
section of the descriptor holds functions that compute values derived from the states
. When states change, the ViewModel
automatically
detects what computed
functions need to re-evaluate.
computed: {
tasksRemainingLabel: function (){
var count = this.tasks.filter(function(x) { return !x.isDone; }).length;
return "There are " + count + " remaining tasks."
}
}
Inside functions of computed values (as well as for methods), the this
parameter refers to the context object of the ViewModel
. The context object contains:
- All the states as properties where
get
/set
manipulates the hidden observable. - All computed states as properties where
get
returnes an up to date value. - All methods as functions that are called on the context object.
Sometimes we want state lists where we can perform incremental changes (such as add
and remove
) without invalidating the entire list. If we use
a regular JavaScript array to back our list, ViewModel
can't detect incremental changes.
To create an observable list, you can initialize a states
variable to an explicit Observable
instance.
states: {
friends: Observable()
}
In this case, the given observable will be used directly in context object instead of a hidden one. Accessing this.friends
in a computed
property or method will return the Observable
.
The created
method is called when the ViewModel
is initialized. You can e.g. use this to populate the state.
created: function() {
for (var i = 0; i < 30; i++) {
this.friends.add({
name: "Generic friend " + i,
avatar: "Assets/avatar" + ((Math.random()*4)+1) ".png"
})
}
}
The methods
section hold functions that the view can call to make logical operations on the component.
methods: {
addTask: function() {
tasks.add({ label: this.newTask, isDone: false });
this.newTask = "";
}
},
The onchanged
section holds functions that react to changes in UX properties (ux:Property
) on the component.
onchanged: {
Parameter: function(p) {
console.log("The parameter to this page changed to " + JSON.stringify(p));
}
}
The
onchanged
feature depends on proposed Fuse changes not yet in production.