knockoutjs save multiple viewmodels from one function? - javascript

I have spent the last few days researching the following KnockoutJS issue.
I have a page with 3 viewmodels on. I am using div's with id's to specify the bindings i.e:
ko.applyBindings(new viewModel(datasource), $("#sectionQualifications")[0]);
I am also using RequireJS which has really helped with making my app modular and works well with KnockoutJS of course.
My question relates to having (as mentioned) 3 viewmodels on my page.. none overlap, but each viewmodel has a SAVE function. So a quick look at one of the viewmodel snippets:
function viewModel(data) {
self = this;
self.quals = ko.observableArray(data),
self.addQual = function () {
self.quals.push(new qualification());
},
self.remove = function (item) {
// Remove from the database IF we have an actual record in our viewmodel
if (item.Id !== 0) {
dataservice_qualifications.deleteEntity(item.Id, ko.toJSON(item),
{
success: function (ret) {
common.notifyOK('Qualification removed');
},
error: function (err) {
common.notifyError('Cannot remove that qualification');
console.log('Qualification Remove Error', err);
console.log('Remove error object', this.Id);
}
}
);
}
// Remove from the actual view model
self.quals.remove(item);
}
// Save and move on.. we need to iterate through the qualifications, update any existing rows (ID NOT 0) or
// add new entries (ID IS 0)
self.save = function () {
var saveData = ko.toJS(this.quals);
for (var i in saveData) {
// New qualification entry
if (saveData[i].Id === 0) { // New qualification entry
dataservice_qualifications.postEntity(ko.toJSON(saveData[i]),
{
success: function (ret) {
},
error: function (error) {
common.notifyError('Cannot add qualification ' + saveData[i].qualificationName);
console.log('Qualification add error', error);
}
}
);
} // eof NEW qualification
if (saveData[i].Id > 0) {
dataservice_qualifications.putEntity(saveData[i].Id, ko.toJSON(saveData[i]),
{
success: function (ret) {
},
error: function (error) {
common.notifyError('Cannot update qualification ' + saveData[i].qualificationName);
console.log('UPDATED: ERROR:', error);
}
}
);
} // eof UPDATED qualification
} // eof saveData loop
common.notifyOK('Qualifications updated');
} // eof savenext function
return;
};
So from that above sample, I would have 2 other viewmodels that are similar, which have the SAVE function as above. So of course I want to use say jQuery to click a button and save all 3 viewmodels (i.e. via that SAVE function on each).
Because I am using RequireJS, I have tried exposing a "public" function that in turn tries to internally call the viewModel.save() function as follows in this snippet:
function saveModel() {
viewModel.save();
}
// PUBLIC INTERFACE
return {
saveModel: saveModel,
loadViewModel: koMapData
}
So the theory being I can call the "saveModel" function from wherever which should trigger the save of the viewmodels?
Any help really appreciated. By the way I have gone down the path of trying to create the viewmodel like:
var viewModel = {
save: function() {
blah blah...
}
}
however no real luck in that either? I am sure I am missing something simple, because I would think you could/should be able to trigger a function from a viewmodel externally somehow?
EDIT
Just for reference, the models DO NOT overlap..
Thanks in advance,
David.

You can merge view models in an object like this:
var mainVModel = {
vModel1: { // has save method},
vModel2: { // has save method},
saveAll : function(){
mainVModel.vModel1.save();
mainVModel.vModel2.save();
}
}
ko.applyBindings(new mainVModel());

Actually thanks #XGreen for your suggestion, and I can see that working well, however I am using requirejs, and so my app structure isn't quite a match.
However, I have been successful in the following solution:
First, I created the viewmodel in a slightly different way..
var viewModel = {
quals: ko.observableArray([]),
addQual: function () {
viewModel.quals.push(new qualification());
},
remove: function (item) {
// Do the remove bit..
},
save: function () {
var saveData = ko.toJS(viewModel.quals);
// Do the save stuff..
} // eof savenext function
}; // eof viewModel def
So the viewmodel is defined, and then I had a helper function to access just the SAVE function of the viewmodel:
// Private: Allows external access to save the viewmodel
var viewModelFunctions = {
saveModel: function () {
viewModel.save();
}
}
.. and then finally, because I am using the revealing module pattern within the requirejs structure, I created a public interface function as follows:
function koMapData(incomingData) {
datasource = (incomingData === null) ? [new qualification()] : incomingData;
viewModel.quals = ko.observableArray(ko.toJS(datasource));
ko.applyBindings(viewModel, $("#sectionQualifications")[0]);
}
// PUBLIC INTERFACE
return {
viewModelFunctions: viewModelFunctions,
loadViewModel: koMapData
}
So you can see the last part with the viewModelFunctions, which means in another module (or wherever) I can reference/trigger the SAVE function remotely by:
mymodule.viewModelFunctions.saveModel()
Which also means (in my case because I have 3 viewmodels I need to trigger save for from one event) I can have the flexibility of saving when/how I want to. Obviously it would be good to return any errors too etc up to the calling module, but that is in principle how it works for me.

Related

JS Revealing Pattern event undefined issue

I am using the modular design pattern for JS and I keep running into issues when using arguments bound functions. I have a particular function that I would like to bind to different events to keep from having to write the function for each bound event. The only difference in the function, or the argument, is the table that will be updated. The problem is that when I build a function with the arguments I need and pass those arguments to bound events, I get an undefined error, in the console, on load. Keep in mind, I want to stick with this design pattern for the security it offers.
Here is my JS:
var Users = (function(){
var $addRoleForm = $('#addUserRole');
var $rolesTableBody = $('#table-roles tbody');
$addRoleForm.submit(ajaxUpdate(event, $rolesTableBody));
function ajaxUpdate(event, tableName) {
event.preventDefault();
event.stopPropagation();
var url = this.action;
var data = $(this).serialize();
var $this = $(this);
$.ajax({
type: 'POST',
url: url,
dataType: 'json',
data: data,
success: function(data) {
if(data.st === 0){
$messageContainer.html('<p class="alert alert-danger">' + data.msg + '</p>');
setTimeout(function(){
$messageContainer.hide();
}, 7000);
} else {
$messageContainer.html('<p class="alert alert-success">' + data.msg + '</p>');
tableName.fadeOut().html('').html(data.build).fadeIn();
$this.find('input').val('');
setTimeout(function(){
$messageContainer.hide();
}, 7000);
}
},
error: function(xhr, status, error){
console.log(xhr.responseText);
}
});
}
})();
Here is the error I get in the console, on load:
Uncaught TypeError: Cannot read property 'preventDefault' of undefined
I have tried to bind the event like this: $addRoleForm.on('submit', ajaxUpdate(event, $rolesTableBody)); and receive the same results.
Any ideas how to fix this?
You're seeing that issue, because the way you have it written now, ajaxUpdateexecutes, returns undefined and THEN passes undefined to the event listener, so you're basically doing this: $addRoleForm.submit(undefined).
2 Choices here:
1) You can wrap it in an anonymous function:
$addRoleForm.submit(function(event) {
//pass the value of "this" along using call
ajaxUpdate.call(this, event, someValue);
});
$someOtherForm.submit(function(event) {
//pass the value of "this" along using call
ajaxUpdate.call(this, event, someOtherValue);
});
2) You can set the first argument in-advance using bind:
$addRoleForm.submit(ajaxUpdate.bind($addRoleForm, someValue));
$someOtherForm.submit(ajaxUpdate.bind($someOtherForm, someOtherValue));
Using this way, you're binding the value of this to be $addRoleForm, setting the first argument to always be someValue, so it's the same as:
ajaxUpdate(someValue, event) {
//value of "this" will be $addRoleForm;
}
To pass the event, and the custom argument, you should be using an anonymous function call
$addRoleForm.submit(function(event) {
ajaxUpdate(event, $rolesTableBody));
});
This is by far the easiest and most readable way to do this.
What you're doing right now equates to this
var $addRoleForm = $('#addUserRole');
var $rolesTableBody = $('#table-roles tbody');
var resultFromCallingFunction = ajaxUpdate(event, $rolesTableBody); // undefined
$addRoleForm.submit(resultFromCallingFunction);
Where you're calling the ajaxUpdate function, as that's what the parentheses do, and pass the returned result back to the submit callback, which in your case is undefined, the default value a function returns when nothing else is specified.
You could reference the function, like this
$addRoleForm.submit(ajaxUpdate);
but then you can't pass the second argument
The question refers to the Revealing Module pattern. Benefit of using this design is readability. Going with the anon function may work, but defeats the overall purpose of the module pattern itself.
A good way to structure your module to help maintain your scope is to setup helper functions first, then call a return at the end.
Example use case with events:
var User = function() {
// local VARS available to User
var addRoleForm = document.querySelector('#addUserRole');
var rolesTableBody = document.querySelector('#table-roles tbody');
// Helper function 1
function ajaxUpdate(tableName) {
...
}
// Helper function 2
function someFunc() {
...
}
function bindEvents() {
addRoleForm.addEventListener('submit', ajaxUpdate, false);
addRoleForm.addEventListener('click', someFunc, false);
}
function init() {
bindEvents();
}
return {
runMe:init
}
}().runMe();
Helps to "modularize" your workflow. You are also writing your revealing pattern as an IIFE. This can cause debugging headaches in the future. Editing the IIFE to instead invoke via the return is easier to maintain and for other devs to work with and learn initially. Also, it allows you to extend outside of your IFFE into another Module, example:
var Clothes = function() {
function anotherFunc() {
...
}
init() {
User.runMe();
anotherFunc();
}
return {
addClothes: init
}
}().addClothes();
I hope this helps to give you a better understanding of how/when/why to use the JS revealing pattern. Quick note: You can make your modules into IIFE, that's not a problem. You just limit the context of the scope you can work with. Another way of doing things would be to wrap the var User and var Clothes into a main module, and then make that an IIFE. This helps in preventing polluting your global namespace.
Example with what I wrote above:
// MAIN APPLICATION
var GettinDressed = (function() {
// MODULE ONE
///////////////////////////
Var User = function() {
// local VARS available to User
var addRoleForm = document.querySelector('#addUserRole');
var rolesTableBody = document.querySelector('#table-roles tbody');
// Helper function 1
function ajaxUpdate(tableName) {
...
}
// Helper function 2
function someFunc() {
...
}
function bindEvents() {
addRoleForm.addEventListener('submit', ajaxUpdate, false);
addRoleForm.addEventListener('click', someFunc, false);
}
function init() {
bindEvents();
}
return {
runMe:init,
style: someFunc
}
}();
// MODULE TWO
//////////////////////////
var Clothes = function() {
function anotherFunc() {
...
}
init() {
User.style();
anotherFunc();
}
return {
dressUp: init
}
}();
// Define order of instantiation
User.runMe();
Clothes.dressUp();
}());

Understanding Complex Scope in Javascript Modules and Plugins

I know there's a lot of questions on Stack about JS Scope... but I ran into a specific problem that I'm unable to wrap my head around. I have a Javascript module that looks something like this (albeit dramatically simplified):
module.exports = {
$company: $('#id_company'),
$companyCtrl: null,
$jobType: $('#id_job_type'),
$jobTypeCtrl: null,
init: function() {
var _this = this;
this.$companyCtrl = this.$company.selectize({
onChange: function(value) {
_this.companyChanged(value);
}
})[0].selectize;
},
companyChanged: function() {
// Company changed has been fired and does a few things
// before it calls this:
this.updateJobType();
},
updateJobType: function() {
var _this = this;
$.ajax({
url:'/ajax-url',
data: {
'id': this.companyID
}
})
.done(function(data) {
// If our job type selectize() instance hasn't been setup,
// then create it now
if (_this.$jobTypeCtrl === null) {
// ------------
// PROBLEM BLOCK
_this.$jobTypeCtrl = _this.$jobType.selectize({
onChange: function(value) {
if (_this.currentModel !== 'wire_add') {
_this.jobTypeChanged(value);
}
}
})[0].selectize;
// ------------
}
// Reload and re-enable input
_this.$jobTypeCtrl.reloadFromOriginalInput();
_this.$jobTypeCtrl.enable();
});
},
}
Now, here's what I don't understand, if I move that "PROBLEM BLOCK" outside of the Ajax call, and put it back up into init(), it works fine. However, as far as I can tell, in it's current location, the scope (_this = this) is the exact same as it would be up in the init function.
And to be more specific, the problem I'm experiencing is that the "onChange" handler never fires when the code is inside of the Ajax handler, but the plugin instance is still created and functions as it otherwise should. However, if I move it up to the init(), the onChange handler fires without any other changes to the code
Any help to get me to wrap my head around this would be greatly appreciated. Thank you!
I had a similar issue, where you start chasing your own tail using objects.
The power of using modules, is that they have their own context. So once compiled, the file knows what vars and funcs are residing inside; this negates the need to track this bouncing from function to function, which becomes a nightmare, once you involve async callbacks.
I recommend rewriting your module with vars at the top and functions, so it's easier to call any function without trying to pass the correct _this/self context from here, there and everywhere.
Here's an untested re-write:
module.exports = {
var $company = $('#id_company'),
$companyCtrl = null,
$jobType = $('#id_job_type'),
$jobTypeCtrl = null;
function init() {
$companyCtrl = $company.selectize({
onChange: function(value) {
companyChanged(value); // <== invoke any function and treat them as black-box code
}
})[0].selectize;
}
function companyChanged() {
// Company changed has been fired and does a few things
// before it calls this:
updateJobType();
}
function updateJobType() {
$.ajax({
url:'/ajax-url',
data: {
'id': companyID
}
})
.done(function(data) {
// If our job type selectize() instance hasn't been setup,
// then create it now
if ($jobTypeCtrl === null) {
// ------------
// PROBLEM BLOCK
$jobTypeCtrl = $jobType.selectize({
onChange: function(value) {
if (currentModel !== 'wire_add') {
jobTypeChanged(value);
}
}
})[0].selectize;
// ------------
}
// Reload and re-enable input
$jobTypeCtrl.reloadFromOriginalInput();
$jobTypeCtrl.enable();
});
}
}

Knockoutjs nested view models

I'am using Knockoutjs and have a problem getting access to root/parent models data in a submodel. The data I get via Ajax.
The Ajax success creates a new WeekViewModel and this creates a few RowViewModels. And here is my Problem, at this time week is not defined.
After the site is rendered, I can get the Infos over week.
The only solution I've found is pasting the parent and root into the submodel.
But this works not so well cause at the initialsation, the parent is a plain js Obeject. After the site is rendered, and I will paste another rowViewModel from a click event, the parent is a knockout object.
Can anyone give me some suggestions where I'va made a mistake? Or whats a way to get this fixed?!
Here's my code:
$(function() {
var week;
var weekData;
$.when(
$.get('js/dataNew.json', function (res) {
weekData = res;
}),
// another calls ...
).then(function () {
week = new WeekViewModel(weekData);
ko.applyBindings(week, $('#content').get(0));
});
function WeekViewModel(data){
var self = this;
var mapping = {
'Rows': {
create: function(options) {
return new RowViewModel(options.data, self);
}
}
};
this.allDays = allDays;
this.cats = new ko.observableDictionary();
// more code ...
ko.mapping.fromJS(data, mapping, this);
};
function RowViewModel(row, parent){
var self = this;
var mapping = {
'RowDays': {
create: function(options) {
return new DayModel(options.data, self, parent);
}
}
};
if(row){
if(!row.DisplayName) {
// need data from the root here
// parent.cats <---
}
}
// more code ...
ko.mapping.fromJS(row, mapping, this);
}
// another submodel ...
});
Update:
fiddle:
http://jsfiddle.net/LkqTU/23750/
updated fiddle with html:
http://jsfiddle.net/LkqTU/23753/

Custom JQuery Plugin Method error

I've been working on writing a custom jquery plugin for one of my web applications but I've been running into a strange error, I think it's due to my unfamiliarity with object-oriented programming.
The bug that I've been running into comes when I try to run the $(".list-group").updateList('template', 'some template') twice, the first time it works just fine, but the second time I run the same command, I get an object is not a function error. Here's the plugin code:
(function($){
defaultOptions = {
defaultId: 'selective_update_',
listSelector: 'li'
};
function UpdateList(item, options) {
this.options = $.extend(defaultOptions, options);
this.item = $(item);
this.init();
console.log(this.options);
}
UpdateList.prototype = {
init: function() {
console.log('initiation');
},
template: function(template) {
// this line is where the errors come
this.template = template;
},
update: function(newArray) {
//update code is here
// I can run this multiple times in a row without it breaking
}
}
// jQuery plugin interface
$.fn.updateList = function(opt) {
// slice arguments to leave only arguments after function name
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var item = $(this), instance = item.data('UpdateList');
if(!instance) {
// create plugin instance and save it in data
item.data('UpdateList', new UpdateList(this, opt));
} else {
// if instance already created call method
if(typeof opt === 'string') {
instance[opt](args);
}
}
});
}
}(jQuery));
One thing I did notice when I went to access this.template - It was in an array so I had to call this.template[0] to get the string...I don't know why it's doing that, but I suspect it has to do with the error I'm getting. Maybe it can assign the string the first time, but not the next? Any help would be appreciated!
Thanks :)
this.template = template
Is in fact your problem, as you are overwriting the function that is set on the instance. You end up overwriting it to your args array as you pass that as your argument to the initial template function. It basically will do this:
this.template = ["some template"];
Thus the next time instance[opt](args) runs it will try to execute that array as if it were a function and hence get the not a function error.
JSFiddle

Passing collection.fetch as a named function to collection.bind does not work

I have two Backbone collections. I want to bind to the reset event one one. When that event is fired, I want to call fetch on the second collection, like so:
App.collections.movies.bind("reset", App.collections.theaters.fetch);
The second fetch never fires though. However, if I pass an anonymous function that calls theaters.fetch, it works no problem:
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
Any idea why this might be the case?
Heres my full code. I'm not showing any of the models or collections, because it's a lot of code, but let me know if you think that might be the source of the problem:
var App = {
init: function () {
App.collections.theaters = new App.Theaters();
App.collections.movies = new App.Movies();
App.events.bind();
App.events.fetch();
},
events: {
bind: function () {
App.collections.theaters.bind("reset", App.theaterManager.assign);
App.collections.movies.bind("reset", function () { App.collections.theaters.fetch(); });
},
fetch: function () {
App.collections.movies.fetch();
}
},
collections: {},
views: {},
theaterManager: {
// Provide each model that requires theaters with the right data
assign: function () {
// Get all theaters associated with each theater
App.theaterManager.addToCollection("theaters");
// Get all theaters associated with each movie
App.theaterManager.addToCollection("movies");
},
// Add theaters to a collection
addToCollection: function (collection) {
App.collections[collection].each(function (item) {
item.theaters = App.theaterManager.getTheaters(item.get(("theaters")));
});
},
// Returns a collection of Theaters models based on a list of ids
getTheaters: function () {
var args;
if (!arguments) {
return [];
}
if (_.isArray(arguments[0])) {
args = arguments[0];
} else {
args = Array.prototype.slice.call(arguments);
}
return new App.Theaters(_.map(args, function (id) {
return App.collections.theaters.get(id);
}));
}
}
};
$(function () {
App.init();
});
This all has to do with function context. It is a common confusion with the way functions are called in Javascript.
In your first way, you are handing a function to be called, but there is no context defined. This means that whoever calls it will become "this". It is likely that the equivalent will be of calling App.collections.movies.fetch() which is not what you want. At least, I am guessing that is what the context will be. It is difficult to know for sure... it might be jQuery, it might be Backbone.sync. The only way to tell is by putting a breakpoint in the Backbone.collections.fetch function and print out the this variable. Whatever the case, it won't be what you want it to be.
In the second case, you hand it a function again but internally, you specify the context in which the function is called. In this case, fetch gets called with App.collections.theaters as the context.
... was that clear?

Categories

Resources