Let's say I have the following durandal code:
define(['durandal/app', 'plugins/http'], function (app) {
var vm = function(){
var self = this;
self.myData = ko.observableArray([]);
}
};
vm.activate = function() {
this.myData([]);
};
vm.deactivate = function(){
};
return vm;
};
I know by returning a constructor function, every time the view model is activated, it will
return a new instance.
My question is: is there anyway that when I access, if there's myData()
from previous access, then I don't want to set this.myData([]);
but to use the previous myData()
?
I know by returning a singleton object can do it, but if I want to keep the constructor function, can I do it?
Another question, what's the point of having activate and deactivate in the above code, if you are going to get new instance, therefore guaranteed a 'clean' object anyway?
There are several approaches you can take, which I enumerate below.
For all of the examples below, consider that Projects is the topic of choice.
INJECTION (NO VIEW)
With this approach, we inject a ProjectsServices module (singleton) into a Projects module (instance). But this approach works only if ProjectsServices does not also offer up one or more views. Below, I'll show you what we can do if our services module also, itself, offers one or more views.
ProjectsServices ViewModel (singleton)
sevicesProjects.js
define('projectsServices', [],
function() {
var myData = null; //this may or may not be an observable
return {
myData: myData //we return an object literal, which is what makes this module a singleton
};
}
);
Projects ViewModel (instance)
projects.js
define('projects', ['projectsServices'],
function(services) {
//Constructor
var Projects = function () {
this.myData = ko.observable(); //this may or may not be an observable
};
Projects.prototype.activate = function (activationData) {
this.myData(services.myData);
};
Projects.prototype.detached = function () {
services.myData = this.myData(); /store back to the services module for later use
};
return Projects; //we return a constructor function, which is what makes this module an instance
}
);
HOST-CLIENT (VIEW)
With this approach, the Projects module is composed inside of the ProjectsServices module, and we pass myData
back and forth through an observable on activationData
. Also, this approach assumes that the services module is not only offering code services, but also view services. A global "Add Contact" form that pops over other forms is an example of a view-based services module. And, of course, the "Add Contact" view would have a viewModel behind it, which represents the code services for adding a contact.
ProjectsServices ViewModel (singleton)
servicesProjects.js
define('projectsServices', [],
function() {
var myData = ko.observable(); //should be an observable
}
);
ProjectsServices View
servicesProjects.html
/*We bring in the Projects module through composition and pass in the observable, `myData`, itself (not `myData()`, but `myData`)*/
<div>
<div data-bind="compose: {model: 'viewmodels/projects', activationData: myData}">
</div>
</div>
Projects ViewModel (instance)
projects.js
define('projects', [],
function() {
//Constructor
var Projects = function () {
this.myDataLocal = ko.observable(); //this may or may not be an observable
this.myDataFromServices = null;
};
Projects.prototype.activate = function (activationData) {
this.myDataFromServices = activationData
this.myDataLocal(activationData());
};
Projects.prototype.detached = function () {
this.myDataFromServices(this.myDataLocal()); /store back to the services module for later use
};
return Projects; //we return a constructor function, which is what makes this module an instance
}
);
Projects View
projects.html
/*There are infinite number of ways to bind to your myDataLocal*/
<div>
<div data-bind="text: myDataLocal}">
</div>
</div>
PUBLISH-SUBSCRIBE
With this approach, we leverage Durandal's built-in pub/sub facilities through app
. This approach can be used either with Injection or Host-Client given above. The strategy is to publish a request message from the activate
handler of the instance module, and receive a reply message within the same handler, the purpose of both messages to request and supply myData
(which was saved off earlier, presumably). When we're ready to save myData
back to the services module, we send another message with myData
as the payload.
ProjectsServices ViewModel (singleton)
servicesProjects.js
define('projectsServices', ['durandal/app'],
function(app) {
var
myData = null, //this may or may not be an observable
activate = function () {
app.on('requestForMyData').then( function () {
app.trigger('responseMyData', myData);
});
app.on('storeMyData').then( function (data) {
myData = data; //where 'data' is the payload
});
},
detached = function () {
app.off('requestForMyData');
app.off('storeMyData');
};
return {
myData: myData, //we return an object literal, which is what makes this module a singleton
activate: activate,
detached: detached
};
}
);
Projects ViewModel (instance)
projects.js
define('projects', ['durandal/app'],
function(app) {
//Constructor
var Projects = function () {
this.myData = ko.observable(); //this may or may not be an observable
};
Projects.prototype.activate = function () {
var that = this;
app.on('responseMyData').then( function (data) {
that.myData(data);
});
app.trigger('requestForMyData'); //no payload
};
Projects.prototype.detached = function () {
app.trigger('storeMyData', this.myData());
app.off('responseMyData');
};
return Projects; //we return a constructor function, which is what makes this module an instance
}
);
The views don't change in this case, so they're not provided here.
MESSAGE BUS
This approach is virtually identical to the pub-sub approach, except that we're using a client-side message bus, such as postal.js. It's more refined, as you will see. It also happens to be the approach that we take in production. This approach should be used with the Host-Client approach above, only we're simply passing in a message channel, not the data itself.
- You can download postal.js here.
- See my (@estaylorco) exchanges with Jim Cowart (@ifandelse) on Github here, here, and here.
- See an RC of postal.request-response (which I do not use below since it's in preview), along with my exchanges there. postal.request-response was something I had asked for (two-way channels), and it's for just the scenario that you're using. It greatly simplifies request-response scenarios.
ProjectsServices ViewModel (singleton)
servicesProjects.js
define('projectsServices', ['postal'],
function(postal) {
var
outletMessageChannel = 'someuniqueidentifier',
subscriptions = [],
myData = null, //this may or may not be an observable
activate = function () {
var that = this;
subscriptions.push(postal.subscribe({
channel: outletMessageChannel,
topic: 'request.mydata',
callback: function () {
postal.publish({
channel: outletMessageChannel,
topic: 'response.mydata',
data: that.myData
});
}
}));
subscriptions.push(postal.subscribe({
channel: outletMessageChannel,
topic: 'store.mydata',
callback: function (data) {
that.myData = data;
}
}));
},
detached = function () {
//I'm using underscore.js here, but you can use any approach to iterate over subscriptions
_.each(subscriptions, function(sub) {
sub.unsubscribe();
sub.callback = null;
sub = null;
});
subscriptions = null;
};
return {
myData: myData, //we return an object literal, which is what makes this module a singleton
activate: activate,
detached: detached
};
}
);
ProjectsServices View
servicesProjects.html
/*We bring in the Projects module through composition and pass in the message channel*/
<div>
<div data-bind="compose: {model: 'viewmodels/projects', activationData: outletMessageChannel}">
</div>
</div>
Projects ViewModel (instance)
projects.js
define('projects', ['postal'],
function(postal) {
//Constructor
var Projects = function () {
this.subscriptions = [];
this.outletMessageChannel = '';
this.myData = ko.observable(); //this may or may not be an observable
};
Projects.prototype.activate = function (activationData) {
this.outletMessageChannel = activationData;
var that = this;
subscriptions.push(postal.subscribe({
channel: this.outletMessageChannel,
topic: 'response.mydata',
callback: function (data) {
that.myData(data);
}
}));
postal.publish({
channel: this.outletMessageChannel,
topic: 'request.mydata',
data: null //no payload
});
};
Projects.prototype.detached = function () {
postal.publish({
channel: this.outletMessageChannel,
topic: 'store.mydata',
data: this.myData()
});
//I'm using underscore.js here, but you can use any approach to iterate over subscriptions
_.each(this.subscriptions, function(sub) {
sub.unsubscribe();
sub.callback = null;
sub = null;
});
this.subscriptions = null;
};
return Projects; //we return a constructor function, which is what makes this module an instance
}
);
Note that the Projects view doesn't change in this case, so it is not included here.
Yes, you can do that.
For example, in our application, each module has two parts: a singleton module following the naming convention of XxxServices, and an instance module, called Xxx, where Xxx is the canonical name of your module:
- ProjectsServices (singleton)
- Projects (instance)
There are [at least] two ways to make services available to instances: through Durandal composition, passing in an activationData
object, and through dependency injection using RequireJS.
(Another way is to use a client-side message bus, such as postal.js, under a request-response scenario. But that's a horse of a different color).
If you're not familiar with these techniques, I can elaborate if you wish.