I'm having an issue with a helper function inside my Backbon.js View. When it's run, it dies with the following error message about the first line of the "addCalc" function:
TypeError: this.getCalcValue is not a function
It's really puzzling because in the "initialize" function defined just above, all the functions seem to be defined. It feels like I'm calling the sibling method wrong, and the "initialize" method is an exception where "this" can be used to reference the object.
Is there something wrong/missing with the following code, or something I missed with the backbone documentation?
CalcView = Backbone.View.extend({
el: $("#calcView"),
initialize: function () {
this.resetCalc();
},
addCalc: function (model) {
var cost = this.getCalcValue(model.get('currentCost'));
var custom = this.getCalcValue(model.get('customProgram'));
var variables = { id: model.get('id'),
category: model.get('category'),
shortDesc: model.get('shortDescription'),
description: model.get('description'),
currentCost: cost,
customProgram: custom,
};
var template = _.template($('#calc_template').html(), variables);
$("#calc_payload").append(template);
},
resetCalc: function(models) {
$("#calc_payload tr").remove();
},
removeCalc: function(model){
$("#calc_payload #" + model.get('id')).remove();
},
updateCalcs: function(model) {
var cost = model.get('currentCost');
var custom = model.get('customProgram');
$("#" + model.get("id") + " .currentCost").text(this.getCalcValue(cost));
$("#" + model.get("id") + " .customProgram").text(this.getCalcValue(custom));
/*var currentCostSum = 0;
var customProgramSum = 0;
$("#calc_payload .currentCost").each(function() {
var temp = Number(($(this).text()).replace(/[^0-9\.]+/g, ""));
if (!isNaN(temp))
currentCostSum += temp;
});
$("#calc_payload .customProgram").each(function() {
var temp = Number(($(this).text()).replace(/[^0-9\.]+/g, ""));
if (!isNaN(temp))
customProgramSum += temp;
});
$("#calc_footer .currentCost").text("$" + ((currentCostSum == 0) ? " -- " : CurrencyFormatted(currentCostSum.toFixed(2))));
$("#calc_footer .customProgram").text("$" + ((customProgramSum == 0) ? " -- " : CurrencyFormatted(customProgramSum.toFixed(2))));*/
},
getCalcValue: function(value) {
if (typeof value == 'string' || value instanceof String)
return value.toString();
else if (isNaN(value))
return "$ -- ";
else
return "$" + value.toFixed(2);
},
});
The code that executes the "addCalc" function is driven by a backbone collection. Basically, when the collection is added to, the CalcView.addCalc is called
Calculations = Backbone.Collection.extend({
model: Calculation,
//This is our Friends collection and holds our Friend models
initialize: function (models, options) {
this.on("add", options.onAdd);
this.on("remove", options.onRemove);
this.on("reset", options.onReset);
//Listen for new additions to the collection and call a view function if so
}
});
//This is where addCalc is used.
var calcview = new CalcView();
var calc_collection = new Calculations( null, {
onAdd: calcview.addCalc,
onRemove: calcview.removeCalc,
onReset: calcview.resetCalc
});
In your initialize function add this line of code:
_.bindAll(this,'addCalc');
This will bind this to be your CalcView for the addCalc function. You can put multiple comma separated method names in there if you need to bind more than one function...
See Underscore's documentation on it here.
When you bind events on collection you can send the context as third argument. Try sending one more option property as your calcview and pass it as context.
this.on("add", options.onAdd, options.calcview);
this.on("remove", options.onRemove, options.calcview);
this.on("reset", options.onReset, options.calcview);
Related
I have page using knockout, which has a searchfield, selectedvalue from a dropdown, and pagenumber..
All these are initialized at set to defaultvalues, especially for first run / page access..
The problem is that i dont understand why i'm getting the following error
"self.selectedTeamId is not a function()"
I know.. this has to be something with the "order of things", so that when it's being used, it has NOT been initialized yet.
Can someone correct my mistake ?
CODE :
$(document).ready(function() {
var ViewModel = function() {
var self = this;
self.photos = ko.observableArray();
self.selectedTeamId = ko.observable(0);
self.searchString = ko.observable('');
self.pageNumber = ko.observable(1);
self.clearFilters = function() {
self.searchString(''); // set default to ''
self.selectedTeamId(0); // set default to 0
self.pageNumber(1); // set default to 1
self.getPhotos();
};
self.getPhotos = function () {
var photoParams = "?teamId=" + self.selectedTeamId() + "&search=" + encodeURIComponent(self.searchString()) + "&pageNumber=" + self.pageNumber();
$.get("api/Photo/GetPhotos" + photoParams,
function(data) {
self.photos(data);
}, "json");
};
};
var photosModel = new ViewModel();
ko.applyBindings(photosModel, document.getElementById("photoarchive"));
// THE NEXT LINE GIVES THE ERROR (self.selectedTeamId())
var photoParams = "?teamId=" + self.selectedTeamId() + "&search=" + encodeURIComponent(self.searchString()) + "&pageNumber=" + self.pageNumber();
$.get("api/Photo/GetPhotos" + photoParams,
function(data) {
photosModel.photos(data);
}, "json");
});
self is a variable which is local to your ViewModel function. It isn't accessible outside of that function.
As you're storing your ViewModel within your photosModel variable, you can instead access the selectedTeamId observable with:
photosModel.selectedTeamId()
You'll need to do the same with self.searchString() and self.pageNumber().
That said, however, you may as well just call photosModel.getPhotos() instead of duplicating the entire function outside of the ViewModel scope.
I have simple situation and can't understand why variable that I pass to function always undefined.
var ProjectItemView = Backbone.Marionette.ItemView.extend({
template: "#ProjectItemTemplate",
initialize: function () {
var id = this.model.get('project_id');
$.getJSON('service/api.php/projects/' + id + '/progress').done(function (data) {
this.renderProgress('4'); //<== pass here
});
},
renderProgress: function (why) {
alert(why); //<== undefined
...
},
...
});
I expect that it equals '4'. In next step I want to pass "data" but now I realize that I can't pass anything.
Since you're invoking renderProgress on the return of $.getJSON you can simply provide the function reference to the done()method of the returned jQuery Promise. Your code would look like this:
var ProjectItemView = Backbone.Marionette.ItemView.extend({
template: "#ProjectItemTemplate",
initialize: function () {
var id = this.model.get('project_id');
$.getJSON('service/api.php/projects/' + id + '/progress')
.done(this.renderProgress);
},
renderProgress: function (data) {
alert(data);
...
},
...
});
If you'll need the view context inside renderProgress (like, for example, to refer to a view property), then provide done() a version of renderProgress that's bound to the view context:
$.getJSON('service/api.php/projects/' + id + '/progress')
.done(_.bind(this.renderProgress, this));
where _.bind is an UnderscoreJS function. Read more about it here.
You loose the context in $.getJSON done callback. Try this:
var ProjectItemView = Backbone.Marionette.ItemView.extend({
template: "#ProjectItemTemplate",
initialize: function () {
var id = this.model.get('project_id');
var _this = this;
$.getJSON('service/api.php/projects/' + id + '/progress').done(function (data) {
_this.renderProgress('4'); //<== pass here
});
},
renderProgress: function (why) {
alert(why); //<== undefined
...
},
...
});
You don't have access to this inside " $.getJSON( " assign this to any variable and then call "renderProgress" method.
var currentObj = this;
$.getJSON('service/api.php/projects/' + id + '/progress').done(function (data) {
currentObj .renderProgress('4'); //<== pass here
});
because in your case this points to current object of that function and not to view object.
What I want to achieve is to create subscription for model properties. This subscription function should call WebApi via Ajax updating property value in database. For ajax call I need three paramaters: "fieldName", "fieldValue" and "modelId", ajax will update database row based on those three parameters.
I have many properties and all of them need the same functionality, so I do not want to subscribe for each property individually, so I found a following suggestion:
ko.subscribable.fn.withUpdater = function (handler) {
var self = this;
this.subscribe(handler);
//support chaining
return this;
};
Add this is how it is "attached" to observables:
self.ModelId= ko.observable();
self.CompanyName = ko.observable().withUpdater(update);
where update is some js function outside model.
However, I have problem, because I am not able to pass three paramaters to update functions (or also I can say in another words - I need to be able to get viewModel.ModelId property value inside update, as well as propertyName).
function update (propertyName, propertyNewValue, anotherPropertyValue) {
//do ajax update
}
As an example for CompanyName property it will be:
update("CompanyName", "New Company value here", 3),
where
3 == viewModel.ModelId
There might be a better way to do this, but the following will work:
First, add a target object to the withUpdate method:
ko.subscribable.fn.withUpdater = function (handler, target, propname) {
var self = this;
var _oldValue;
this.subscribe(function (oldValue) {
_oldValue = oldValue;
}, null, 'beforeChange');
this.subscribe(function (newValue) {
handler.call(target, _oldValue, newValue, propname);
});
return this;
};
The update subscribe function will get scoped to the target property:
var update = function (propertyName) {
console.log('propname is '+ propname + ' old val: ' + oldvalue + ', new val: ' + newvalue + ', model id: ' + this.ModelId());
}
Now you will need to use it a little differently.
self.CompanyName = ko.observable().withUpdater(update, self, "CompanyName");
An example http://plnkr.co/edit/HhbKEm?p=preview
I couldn't get the scope of the withUpdater function to be that of the object without explicitly passing in the target and a string for the company name.
You can declare your function as a variable outside of the 'fn' scope.
var dataservice = 'my class that has the data calls';
var altFunc = function () {
return ko.pureComputed(function () {
var currentItem = this().filter(function (item) {
// Do knockout stuff here and return your data
// also make calls to the dataservice class
}, this, dataservice);
};
ko.observableArray.fn.someNewFunctionality = altFunc;
I am trying to implement a baseEntity class for entities such as user and groups but isFavourite property can not read the correct Id() from persistanceId() when it is in baseEntity. (type and Id() come out as undefined and surprisingly type has the correct value in confirmDelete)
define(["knockout"], function (ko) {
var app = require('durandal/app');
ko.baseEntity = function (data) {
var self = this;
self.Id = ko.observable();
self.confirmDelete = function () {
var result;
app.showMessage(
'Are you sure you want to delete the ' + self.type + ' ' + self.Name() + '?',
'Deleting ' + self.type, ['Yes', 'No']).then(
function (dialogResult) {
dialogResult === "Yes" ? result = true : result = false;
});
return result;
};
self.persistanceId = function () {
return self.type + '-' + self.Id() + "-IsFavourite";
};
self.isFavourite = ko.observable(false).extend({
persist: self.persistanceId()
});
self.toggleFavourite = function () {
self.isFavourite(!self.isFavourite());
};
}
return {
model: ko.baseEntity
}
});
but if isFavourite instead of being here in baseEntity it is for example part of group then it works fine.
define(["knockout", "models/baseentity"], function (ko, baseEntity) {
var
model = function (data) {
var self = this;
baseEntity.model.call(self);
self.type = 'group';
self.Id(data.Id);
self.Name = ko.observable(data.Name);
self.Description = ko.observable(data.Description);
self.Members = ko.observableArray(data.Members);
self.isFavourite = ko.observable(false).extend({
persist: self.persistanceId()
});
}
return {
model: model
}
});
Could someone explain to me what is going on here and how can I move my base property back in my baseentity as it is shared across various other things too.
I am no javascript master but i would look to decouple the inheritance of your model hierarchy from knockout - does the model behave as expected if you implement it vanilla?
I don't follow why you would want to modify the knockout object itself? I believe Dave Lowe is correct in suggesting that you do this in JavaScript alone. Properties on your model, to the extent that they affect your view, should be observable, but your model doesn't need to be attached to knockout.
Also, consider spending some time at http://objectplayground.com, which has a great tutorial on learning object oriented JavaScript. Your paradigm should look a little more like this:
function Model(obj) {
this.attribute = obj.attribute;
this.observable = ko.observable(obj.observable);
this.localFunction = function(val) {
if (obj.attr == true) this.observable(val);
};
}
Model.prototype.globalFunction = function(data) {
this.observable(data);
};
Notice, in particular, that if the method depends on the local variable, that is, the argument passed to the constructor, then it needs to be defined in the constructor. Otherwise, you should define methods on the prototype.
This paradigm works excellently with require in durandal since you can do the following:
define(function(require) {
var Model = require('model');
var object = new Model({});
})
OK, apparently the easier way to do this subClass business in ko is using ko itself. Who would have thought :)
So now I have defined my base model to be:
define(["knockout"], function (ko) {
var app = require('durandal/app');
ko.baseEntity = function (type, data) {
var self = this;
self.Id = ko.observable(data.Id);
self.Type = ko.observable(type);
self.Name = ko.observable();
self.persistanceId = ko.computed(function () {
return self.Type() + '-' + self.Id() + "-IsFavourite";
});
self.isFavourite = ko.observable(false).extend({
persist: self.persistanceId()
});
self.toggleFavourite = function () {
self.isFavourite(!self.isFavourite());
};
self.confirmDelete = function () {
var result;
app.showMessage('Are you sure you want to delete the ' + self.Type() + ' ' + self.Name() + '?', 'Deleting ' + self.Type(), ['Yes', 'No'])
.then(function (dialogResult) {
dialogResult === "Yes" ? result = true : result = false;
});
return result;
};
}
return {
model: ko.baseEntity
}
});
As you can see I have now added the same data parameter that I pass into my concrete implementation. This is possible as later I will use ko.utils.extend to create an instance of this and extend it:
define(["knockout", "models/baseentity", "config"], function (ko, baseEntity, config) {
var
model = function (data) {
var self = this;
ko.utils.extend(self, new baseEntity.model(config.subClassTypes.user, data));
self.Id(data.Id);
self.FirstName = ko.observable(data.FirstName);
self.LastName = ko.observable(data.LastName);
self.JobTitle = ko.observable(data.JobTitle);
self.UserLevel = ko.observable(data.UserLevel);
self.Groups = ko.observableArray(data.Groups);
self.ImageUrl = data.ImageUrl;
self.Name(self.FirstName() + ' ' + self.LastName());
}
return {
model: model
}
});
another subclass example:
define(["knockout", "models/baseentity", "config"], function (ko, baseEntity, config) {
var
model = function (data) {
var self = this;
ko.utils.extend(self, new baseEntity.model(config.subClassTypes.group, data));
self.Id(data.Id);
self.Name(data.Name);
self.Description = ko.observable(data.Description);
self.Members = ko.observableArray(data.Members);
}
return {
model: model
}
});
This way I have managed to get my methods transferred over to the base and it works fine. I hate answering my own questions, so waiting for someone to add some nice valuable answers I can tick.
I am writing javascript code with revealing prototype pattern for the first time. I am having problems. When I call add function when user clicks add button then it shows me this error in the console.
Uncaught TypeError: Cannot call method 'add' of undefined
How can I solve this problem?
here is my script.js code
$(function () {
var todo = Todo('contents');
$('.addBtn').on('click', function() {
var name = $(this).parent().find('input[type="text"]').val();
todo.add(name);
});
$('.contents').on('click', '.remove', function() {
var el = $(this).parent();
todo.remove(el);
});
$('.contents').on('click', '.update', function() {
var dom = $(this);
todo.addUpdateField(dom);
});
$('.contents').on('click', '.updateBtn', function() {
var el = $(this);
todo.update(el);
});
});
here is my todo.js code
var Todo = function(c) {
this.contents = $('.' + c);
};
Todo.prototype = function() {
var showel = function (d) {
this.contents.prepend(d);
},
add = function (name) {
if(name != "") {
var div = $('<div class="names"></div>')
.append('<span>' + name + '</span>')
.append("<button class='update' class='update'>Edit</button>")
.append("<button class='remove' name='remove'>Remove</button>");
}
return showel(div);
},
addUpdateField = function (dom) {
var name = dom.parent().find('span').text(),
field = $('<input type="text" value="' + name + '" />'),
update = $('<button class="updateBtn">Update</button>');
dom.parent().html('').append(field).append(update);
return;
},
update = function(el) {
var val = el.parent().find('input').val();
el.parent().html('<span>' + val + '</span>')
.append('<button class="update" class="update">Edit</button>')
.append('<button class="remove" class="remove">Remove</button>');
return;
},
remove = function (el) {
return el.remove();
};
return {
add : add,
update : update,
remove : remove,
addUpdateField : addUpdateField
};
}();
Update
After changing
var todo = Todo('contents');
to
var todo = new Todo('contents');
I get this error
Object [object Object] has no method 'add'
update 2
here is my on jsfiddle
You're not properly constructing your object, so it does not have any of the prototypes:
var todo = Todo('contents');
should be:
var todo = new Todo('contents');
Here is an SO question explaining what is happening when you forget the new.
Edit: the way you are defining your prototype functions is messing up the context (what this points to). Try a pattern like this instead:
Todo.prototype = {
method1: function () { ... },
method2: function () { ... }
};
fixed fiddle: http://jsfiddle.net/BagmY/3/
You're trying to assign Todo's prototype to a self-calling function. However, the prototype is just getting assigned to a function and not the expected return object.
Here's your working fiddle. I assigned the prototype directly to an object with your methods.
Also, JS parses from top to bottom. Therefore, define your new instance of Todo after you declare what Todo is.
This is what a self-calling function should look like:
Todo.prototype = (function() {
// ...
return {
add: add,
update: update,
remove: remove,
addUpdateField: addUpdateField
};
})();