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 () {
...
}
Related
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
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 am struggling with when to destroy backbone views. I know I need to destroy the view somewhere, but I am not sure where.
I have the following code in router.js
routes: {
"names/search": "nameSearch",
"companies/search": "companySearch"
},
initialize: function(){
Backbone.history.start();
this.navigate("#/", true);
}
nameSearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var obj = {};
obj.Status = [utils.xlate("On Assignment"), utils.xlate("Candidate")];
var view = new RecordSearchView({ model: obj, el: $(".content") }, { "modelName": "Candidate" });
view.delegateEvents();
});
},
companySearch: function () {
require(["app/views/RecordSearch"], function (RecordSearchView) {
var view = new RecordSearchView({ model: {}, el: $(".content") }, { "modelName": "Company" });
view.delegateEvents();
});
}
And then in RecordSearchView.js I have the following function that is called when a user clicks the search button
doSearch: function () {
require(["app/utils/SearchHelper", "app/models/" + modelName, "app/views/SearchResults"], function (SearchHelper, Model, SearchResultsView) {
var obj = $("#searchForm").serializeArray();
var params = SearchHelper.getQuery(obj);
params["page"] = 1;
params["resultsPerPage"] = 25;
var collection = new Model[modelName + "Collection"]({}, { searchParams: params });
params["Fields"] = collection.getSearchFields();
collection.getPage(params["page"], function (data) {
require(["app/views/SearchResults"], function (SearchResultsView) {
App.Router.navigate(modelName + "/search/results");
var view = new SearchResultsView({ collection: data, el: $(".content") });
view.delegateEvents();
});
});
return false;
});
And SearchResults.js
return BaseView.extend({
init: function () {
this.render();
},
render: function () {
var data = this.collection.convertToSearchResults();
this.$el.html(template(data));
return this;
}
});
The problem is the second time I perform any search (calling the doSearch function from RecordSearch.js). As soon as I perform the second search, the data shown is that belonging to the previous search I performed. (For example I do a name search and it works, then do a company search but the screen shows company search results but then is quickly replaced with name search results).
My questions are
I suspect I need to call some cleanup code on the view before it is re-used. Where is the proper place within a backbone application to run this.
Is there anything wrong with the way I load SearchResults view from within RecordSearch view? SearchResults does not have a path on my router, but it is basically a form post, so I assume it shouldn't?
Any help is appreciated.
This problem is quite common and is known as Zombie Views. Derick Bailey explains this issue very well here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
However unfortunately you can't simply solve it without changing the way you are loading your views.
Because you are loading them inside RequireJS modules that will keep it in the local var scope, you are losing the reference to the views once the route has been fully processed.
In order to solve this problem, you would need to keep the reference of the current view somewhere, and then properly dispose it before calling another view, something like this:
showView: function(view) {
this.currentView && this.currentView.remove();
this.currentView = view;
this.currentView.render();
$('#content').html(this.currentView.el);
}
More about this solution here: http://tiagorg.com/talk-backbone-tricks-or-treats-html5devconf/#/6
I personally suggest you adopting a solution that will take care of this for you, like Marionette.js
It will handle this and quite many other issues, by providing the missing gaps of every Backbone-based architecture.
I am trying to use Durandal to generate a list of data. Because this data will be updated by a dialog I need the list item to represent a view model. For this, view composition seems appropriate, but I must be missing something with how to do this. Note: I am using ASP .NET MVC so my pathing is a bit different. However, at this point, I am not seeing any web requests for my view nor are the debugger; lines in my View Model being hit.
Here is the code:
The HTML:
<tbody data-bind="foreach: workItems">
<!-- ko compose: {model: 'viewmodels/project/workItem', view: 'views/project/workitem'} -->
<!-- /ko -->
</tbody>
Here is the JS for the view model intended to represent each line in the list:
define(['knockout'], function(ko) {
var vm = function () {
var that = this;
this.activate = function(ctx) {
debugger;
};
};
return vm;
});
Here is the code which handles the GET request returning the data:
this.activate = function (ctx) {
var q = null;
if (ctx == undefined) {
q = that._getCurrent();
} else {
q = that._getPath(ctx);
}
q.then(function (response) {
that.workItems(response);
app.trigger('viewmodel:loadingComplete');
});
};
I have tried using system.acquire so as to add VMs to my observable array, this failed to show any results. My thinking was that I need composition so I get the binding to fire for each line. (this may be wrong). I am really looking for an example of this working. Quite perplexed that I have not been able to find anything on SO or Google.
Thanks in advance
1) Check that workItems really does contain data. Use a simple div foreach to output the results.
2) Is there a reason you are not using the Singleton pattern?
3) Return a promise from the activate to make sure the data is loaded before starting the composition.
define(['knockout'], function (ko) {
var items = ko.observableArray([]);
var activate = function() {
console.log("calling composelist:activate");
items.push({name:"Hello"});
items.push({name:"world"});
};
return {
activate: activate,
items: items
};
});
Are you trying to load different v/vm based on a url parameter? If so why not use the basic shell /routes?
Can you submit your code?
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.