Is it wise to put your ajax calls in your Knockout ViewModel or should it instead be placed in a Model? I've come up with a few approaches but none feel completely right.
Approach 1 - ViewModel Only
window.someDataVM = function() {
var self = this;
//used to enable loading indicator
self.pendingLoad = ko.observable(true);
self.myData = ko.observableArray();
self.load = function() {
//make ajax call and populate myData observable array
}
}
Advantages
Simplest code structure - easier to maintain
Disadvantages
No reuse for data retrieval
Approach 2 - Model and ViewModel With Callback
window.someDataVM = function() {
var self = this;
//used to enable loading indicator
self.pendingLoad = ko.observable(true);
self.myData = ko.observableArray();
self.load = function() {
someDataM.load(function(data) {
//populate myData observable array
});
}
}
window.someDataM = function() {
return {
load: function(callback) {
//get data via ajax and return via callback
}
}
}
Advantages
More code reuse on data retrieval (i.e. one place to load someData)
Simpler interface that approach 3
Disadvantages
Uses callbacks
Approach 3 - Model and ViewModel With Knockout Model
window.someDataVM = function() {
var self = this;
//used to enable loading indicator
self.pendingLoad = ko.observable(true);
self.myData = ko.observableArray();
self.load = function() {
someDataM.load();
}
someDataM.isLoaded.subscribe(function(isLoaded) {
if (isLoaded) {
//populate observable array
}
});
}
window.someDataM = function() {
return {
isLoaded: ko.observable(false);
items: [],
load: function() {
//get some data, populate items, set isLoaded
}
}
}();
Advantages
Doesn't use callback
Keeps data code centralized
Disadvantages
Will be complicated to have lots of data entry points (i.e. LoadById, LoadByName, etc, etc.)
I personally don't feel comfortable with self-loading VMs. Thus, I would recommend to load data (model) first, and then pass it to the VM.
Conceptually, it would be something like this:
function loadData() {
//load data, can be asynchronously. Then callback
callback(data);
}
function callback(data) {
var vm = new someDataVM(data);
//do something with VM.
ko.applyBindings(vm);
}
This kind of approach makes even more sense when VMs are created by other VMs (multi-screen applications). Also, this approach emphasizes on model-view-viewModel separation by making a chain of logical dependency:
View => ViewModel => Model
However, VMs can re-load data or make asynchronous calls on user interactions. e.g. user may click a button on the page which loads the current time again. These kinds of interactions will happen inside the existing vm obviously. But the question was related to initial load, which I approach this way.
Related
I am having trouble understanding how to work with Knockout JS Mapping Plugin. I have some nested models (as seen below) and what I am currently doing is just using the ko.mapping.fromJS() in the parent model. What I am noticing is that computed values are not being ...computed.
I tried to understand the "create":
var mapping = {
'children': {
create: function(options) {
return new myChildModel(options.data);
}
}
}
var viewModel = ko.mapping.fromJS(data, mapping);
But right now in my current scenario I am not sure how to implemented.
Current structure:
var ProductModel = function($name, $price, $quantity) {
var self = this;
self.id = ko.observable();
self.name = ko.observable($name);
self.quantity = ko.observable($quantity);
self.price = ko.observable($price);
self.price.total = ko.computed(function() {
return self.price() * self.quantity();
});
};
var CartModel = function (){
var self = this;
// Model Properties
self.id = ko.observable();
self.products = ko.observableArray();
self.fetch = function() {
$.ajax({
url: "route to get the specific cart",
type: "GET",
success: function(data) {
ko.mapping.fromJS(data, {}, self);
}
});
};
// Convert to mapping format
ko.mapping.fromJS(ko.mapping.toJS(self));
};
var ViewModel = ko.validatedObservable(new CartModel());
ko.applyBindings(new ViewModel());
I am not sure how to get the ProductModel to trigger the computed inside the ProductModel, I am not sure if I have to call the ko.mapping.fromJS inside every single Model (I have many more models, I stripped them out to make it simpler).
After the fetch function I alerted the products().length and it actually contains the quantity that was previously saved. but the computed is not showing. How do I implement the create method of knockout mapping plugin for nested observable(and observablearrays) that are models with computed inside of them.
P.S: The reason I have it self.price.total is in order to avoid the toJS send the total key as well. I can't ignore the properties of nested models using ignore:
See this thread for more: Knockout JS Mapping fromJS nested models
How does your ajax response looks like? Does it returns the whole cart, or just the products?
Anyway, if you want to handle your product list as an observableArray of viewmodels you could do something like this:
ko.utils.arrayForEach(products, function(item, index) {
self.products.push(new ProductModel(item.name, item.price, item.qty);
});
If you want to use the mapping plugin, I haven't made a custom implementation of the "create" method, but I would do something like:
var ProductModel = function(product) {
var self = this;
ko.mapping.fromJS(product, {}, self);
self.total = ko.computed(function() {
return self.price() * self.quantity();
});
};
Edit
I have re-read your question, and I think this answer may be what you're looking for, the only downside of that approach, well for me at least, is that you would need to define a mapping config object for each viewmodel with nested viewmodels in your code
I have two javascript modules which act on different parts of the page. Now at moment as you can see I'm using the PubSubJS library to publish and subscribe and transfer data if need be from one module to another module in a decoupled way. But I was thinking whether I can altogether omit the PubSubJS library use JQuery promises(or any other native JQuery method) instead to achieve the same. I'm not so good with JQuery promises hence the need for this question. Can somebody provide me any better solution with JQuery.
var salesOrder = (function() {
"use strict";
var $root, $salesOrderNo, $closeButton;
var _init = function() {
$root = $("#sales-order")
$closeButton = $root.find("#close-button");
_attachEvents();
};
var _attachEvents = function() {
$closeButton.on("click", _closeSalesOrder);
};
var _closeSalesOrder = function() {
PubSub.publish("ui.unloadShell", "closed"); //Here I'm publishing
}
return {
init: _init
}
})();
$(document).ready(salesOrder.init);
And the second module as so
var erpTest = (function() {
"use strict";
var $root, $btnMenu, $shell;
var _init = function() {
$root = $("body")
$btnMenu = $root.find(".menu-button");
$shell = $root.find("#shell");
_attachEvents();
}
var _attachEvents = function() {
$btnMenu.on("click", _loadShell);
PubSub.subscribe('ui.unloadShell', _unloadShell); //Here I'm subscribing
}
var _loadShell = function(evt) {
var url = $(evt.target).data("url");
if (url && url.length) {
$shell.load(url, _loadCompleted);
}
};
var _unloadShell = function(evt, data) {
$shell.html(null); //Here is the subscribed handler
};
var _loadCompleted = function(evt) {
$.each([buttonModule.init, nameModule.init], function(index, func) {
func($shell);
});
};
return {
init: _init
}
})();
$(document).ready(erpTest.init);
I use the PubSub pattern extensively. Your questions are the ones I was looking into a while ago. Here are my comments:
jQuery Promises: Promises are by nature async; do you really want an async channel of communication between components? Using Promises, you'd expect that any subscribers respond properly as your publisher might take action back using .then. Things will become complex as soon as you expect subscribers to respond accordingly to events.
jQuery has .on, .off, .one to publish events; you simply need to pass {} as aggregator. See that topic for further details: Passing an empty object into jQuery function. However jQuery has some overhead compared to a simple pubSub/aggreagator mechanism.
I built several labs of incremental complexity focused on the PubSub pattern that you can consult below. LineApp is the entry point.
https://pubsub-message-component-1975.herokuapp.com
I'm currently developing my first Backbone single page app project and I'm facing an issue.
Basically I have a menu (html select input element) implemented as a View. Its value is used to control pretty much every other data requests since it specifies which kind of data to show in the other Views.
Right now I handle the DOM event and trigger a global event so that every model can catch it and keep track internally of the new value. That's because that value is then needed when requesting new data. But this doesn't look like a good solution because A) I end up writing the same function (event handler) in every model and B) I get several models with the same variable.
var Metrics = Backbone.Collection.extend({
url: "dummy-metrics.json",
model: MetricsItem,
initialize: function () {
this.metric = undefined;
},
setMetric: function (metric) {
this.metric = metric;
globalEvents.trigger("metric:change", this.get(metric));
}
});
var GlobalComplexity = Backbone.Collection.extend({
url: function () {
var url = "http://asd/global.json?metric=" + this.metric;
return url;
}, //"dummy-global.json",
model: GlobalComplexyItem,
initialize: function () {
this.metric = undefined;
this.listenTo(globalEvents, "metric:change", this.updateMetric);
},
updateMetric: function (metric) {
this.metric = metric.get("id");
this.fetch({ reset: true });
}
});
All my other Collections are structured like GlobalComplexity.
What's the cleanest way to solve this problem?
Thank you very much.
Define a global parametersManager. Export an instance (singleton) then require it when you need it.
On "globalupdate" you update the parametersManager then trigger "update" for all your model/collections so they'll look what are the current parameters in the parametersManager.
Well met!
I am playing around with Knockoutjs with the goal of having a single ViewModel, which controls multiple sub-viewmodels. This in order to have more control over the views itself and to prevent putting various parts of my view into their own little place. The code below should explain my idea:
ApplicationViewModel
ApplicationViewModel = function () {
var self = this;
// Context (for laziness' sake, no separate VM)
self.activeProject = ko.observable();
// States
self.projectsLoaded = ko.observable(false);
// State-change events
// Let application know that loading of projects has been called
self.projectsLoaded.subscribe(function (newValue) {
if (newValue === true) {
console.log('Projects have loaded');
} else {
console.log('Projects have not loaded');
}
});
// Let application know that selection of a project has happened
self.activeProject.subscribe(function (newValue) {
if (newValue != null) {
// Notify other viewmodels that a project has been (successfully loaded)
// Use hook-pattern to hook into this event
} else {
// Notify something went wrong- present user with a notification
// Application stops processes that are project-dependant
}
});
self.ProjectViewModel = new ProjectViewModel();
};
ProjectViewModel
ProjectViewModel = function () {
var self = this;
self.projects = ko.observableArray();
self.loadProjects = function () {
// Business logic to retrieve projects, think AJAX
var placeHolderProjects = [];
// Find projects somewhere and load them up!
// If something went wrong, notify parent
if (placeHolderProjects.length > 0) {
self.projects(placeHolderProjects);
$root.projectsLoaded(true);
} else {
$root.projectsLoaded(false);
}
};
self.selectProject = function (projectId) {
if (!projectId) {
$.parent.activeProject = null;
return;
}
// Fetch data for project, stuff like membershipId
var loadProjectResult = magicalLoadFunction(projectId);
if (loadProjectsResult === true) {
$root.activeProject(projectId);
} else {
$root.activeProject(projectId);
}
// Exit
return;
}
/********** Constructor logic
****************************/
self.loadProjects();
};
So basically, what I am looking for, is a way to:
- Control parent/child properties from their respective child/parent inside the viewmodels.
I am looking into AngularJS as well, but I'd really like to get this working in KnockoutJS first :) Immediate problem, is that I can't get $root/$parent to work. I bind the ApplicationViewModel in a $(document).ready() handler, unsure if I have to actually bind the sub-viewmodels to the view as well. I have bound ApplicationViewModel to the body element.
Thanks for reading and, possibly for answering/helping me get on my way :)
The answer provided by #jansommer proved successful.
I changed the following line (added this as a parameter):
self.ProjectViewModel = new ProjectViewModel(this);
And that was what was needed.
Thanks!
I'm working on a knockout.js wizard and need to get data from multiple remote data sources (via AJAX) before I can properly render the drop-down menus in the wizard.
Additionally, there are 4 dropdowns and while #1 and #2 can be loaded up first, #3 and #4 depend on the choices selected in the first two.
So far I've experimented with using jQuery promises and also just nesting data calls and their associated callbacks, but are there any better ways to structure my view model code for the wizard?
Below is some of the data loading code. I'm happy to provide more if needed.
var postobj = {
id: workoutId
};
var getWorkout = $.post("./RowingWorkouts/GetWorkoutUsingId", postobj);
var getDiet = $.post("./Diet/GetDietUsingId", postobj);
var getFeedback = $.post("./RowingWorkouts/GetWorkoutFeedback", postobj);
// When all three are successful - I haven't gotten the when syntax to actually work yet
$.when(getWorkout, getDiet, getFeedback).done(function (workout, diet, feedback) {
//each of the parameter is an array
renderCharts(workout[0], diet[0], feedback[0])
// Here are more dropdowns that depend on the choices from the above ones
self.suggestedWorkouts = ko.observableArray();
// pseudo-code for data call for getting suggested workouts
$.post("./RowingWorkouts/GetSuggested", { id: selectedOldWorkout }, function(result) {
self.suggestedWorkouts(result);
});
});
This goes several levels deeper, and I would prefer avoiding it if at all possible. Are there any design patterns I'm missing or is this plain coded wrong?
You can use lazy loading observable to get data into your viewModel observables, and computed to subscribe on load of the parent level observables.
function ViewModel() {
this.workout = ko.onDemandObservable(ViewModel.prototype.getWorkout, this);
this.diet = ko.onDemandObservable(ViewModel.prototype.getDiet, this);
this.feedback= ko.onDemandObservable(ViewModel.prototype.getFeedback, this);
this.suggestedWorkouts = ko.observable();
ko.computed(ViewModel.prototype.listsLoaded, this);
}
ViewModel.prototype.listsLoaded= function () {
if (this.workout.loaded() && this.diet.loaded() && this.feedback.loaded()) {
this.loadSuggestedWorkouts();
}
}
ViewModel.prototype.getWorkout = function () {
...
}
ViewModel.prototype.getDiet = function () {
...
}
ViewModel.prototype.getFeedback = function () {
...
}
ViewModel.prototype.loadSuggestedWorkouts = function () {
...
}