Probably Another Javascript Closure Issue - javascript

I think I'm having a closure issue, but I'm having a hard time trying to solve it.
I thought that using $.map would prevent any problem.
See the javascript below (require Zepto or jQuery to work) or access the working code https://jsfiddle.net/rowcp07n/4/:
"use strict";
var log = function(){
var output = document.getElementById('output');
for(var i in arguments){
output.append(JSON.stringify(arguments[i]));
output.appendChild(document.createElement('br'));
output.appendChild(document.createElement('hr'));
}
};
function EmptyAction(){
self = this;
self.play = function(){
self.complete();
};
};
function Scene(row, i){
log('Constructing Scene '+i);
var self = this;
self.index = i;
self.steps = $.map(row, function(col, i){
return new SceneStep(row[i], self, i);
});
}
function SceneStep(col, scene, i){
log('Constructing SceneStep '+i+' for Scene '+scene.index);
var self = this;
self.index = i;
self.action = new EmptyAction();
self.action.parentStep = self;
self.action.complete = function(){
log('Complete handler for SceneStep '+ self.index);
};
self.value = col;
}
var data = [
[1,2,3],
[4,5,6],
[7,8,9]
];
function ViewModel(data){
var self = this;
self.scenes = $.map(data, function(row, i){
return new Scene(row, i);
});
}
var vm = new ViewModel(data);
log('Triggering complete of 0-0');
vm.scenes[0].steps[0].action.complete();
log('calling play() of 0-0');
vm.scenes[0].steps[0].action.play();
I expect to see, at the end of the log, "Complete handler for SceneStep 0"
but I'm getting "Complete handler for SceneStep 2"
Can anyone point to me what I'm missing?

As #JJJ pointed, I forgot to declare the var self in class EmptyAction.

Related

How to override a function in a Javascript class, and call the base function

Is it possible to override a function in a Javascript class, and call it's base implementation? I've achieved this by using prototypes, but I'm trying to preserve privacy for some of the data.
This is what I have so far, and it doesn't work. I can see why it doesn't work, but I can't see a way to resolve it. I'm beginning to wonder if this is not possible in javascript (without jumping through a lot of hoops).
Also, I need to support IE11, so can't use ES6.
var NoProto = NoProto || {};
NoProto.Shape = (function(){
var thing = function(name){
var privateData = 'this is a ' + name;
var self = this;
this.base = function(){
return self;
};
this.doStuff = function(){
return privateData;
};
};
return thing;
})();
NoProto.Square = (function(){
var thing = function(colour){
NoProto.Shape.call(this, "square");
this.doStuff = function(){
// this fails (stack overflow)
// ------> how to call the "base" function: doStuff, and preserve the private data?
var val = this.base().doStuff();
return val + ', which is '+ colour;
};
};
thing.prototype = Object.create(NoProto.Shape.prototype);
return thing;
})();
Usage:
var noProtoSqr = new NoProto.Square('blue');
try {
alert(noProtoSqr.doStuff()); // ---> Stack Overflow!
} catch (e){
console.error('There was an error: ' + e);
}
For reference, this is how I got it working with prototypes:
var Proto = Proto || {};
Proto.Shape = (function(){
var thing = function(name){
this._pseudoPrivateData = 'this is a ' + name;
};
thing.prototype._pseudoPrivateData = '';
thing.prototype.doStuff = function(){
return this._pseudoPrivateData;
};
return thing;
})();
Proto.Square = (function(){
var thing = function(colour){
Proto.Shape.call(this, "square");
this._colour = colour;
};
thing.prototype = Object.create(Proto.Shape.prototype);
thing.prototype._colour = '';
thing.prototype.doStuff = function(){
var val = Proto.Shape.prototype.doStuff.call(this);
return val + ', which is '+ this._colour;
};
return thing;
})();
Usage:
var protoSqr = new Proto.Square('blue');
try {
alert(protoSqr.doStuff()); // --> "this is a square, which is blue"
} catch (e){
console.error('There was an error: ' + e);
}
When you use
NoProto.Shape.call(this, "square")
this assigns the Shape's doStuff to the current instantiation, if that's what you want. So, now this.doStuff will reference the original doStuff function from NoProto.shape. If you want to overwrite the doStuff function on the current instantiation while being able to call the original doStuff, save a reference to the old doStuff before assigning to this.doStuff:
var thing = function(colour){
NoProto.Shape.call(this, "square");
const oldDoStuff = this.doStuff;
this.doStuff = function(){
var val = oldDoStuff();
return val + ', which is '+ colour;
};
};
Live snippet:
var NoProto = NoProto || {};
NoProto.Shape = (function(){
var thing = function(name){
var privateData = 'this is a ' + name;
var self = this;
this.base = function(){
return self;
};
this.doStuff = function(){
return privateData;
};
};
return thing;
})();
NoProto.Square = (function(){
var thing = function(colour){
NoProto.Shape.call(this, "square");
const oldDoStuff = this.doStuff;
this.doStuff = function(){
var val = oldDoStuff();
return val + ', which is '+ colour;
};
};
thing.prototype = Object.create(NoProto.Shape.prototype);
return thing;
})();
var noProtoSqr = new NoProto.Square('blue');
try {
console.log(noProtoSqr.doStuff()); // ---> Stack Overflow!
} catch (e){
console.error('There was an error: ' + e);
}

self.Rotate() is not a function in prototype

I've been looking at the other questions that are similar to mine, but I can't seem to find an answer.
The error that I'm having is in the title as well:
Uncaught TypeError: self.Rotate() is not a function
Here's the full code where this happens:
var Card = function(renderer, stage) {
var self = this;
self.name = "None";
self.health = 5;
self.renderer = renderer;
self.stage = stage;
self.sprite = null;
PIXI.loader.add("FirePlace/GW2-Logo.jpg").load(self.Setup);
};
Card.prototype.Setup = function() {
self.sprite = new PIXI.Sprite(PIXI.loader.resources["FirePlace/GW2-Logo.jpg"].texture);
console.log(self.stage);
self.stage.addChild(self.sprite);
console.log("Sprite loaded");
self.renderer.render(self.stage);
self.Rotate();
};
Card.prototype.SetName = function(name) {
self.name = name;
};
Card.prototype.Rotate = function() {
requestAnimationFrame(self.Rotate);
if (self.sprite === null)
console.log("Sprite is null");
if (self.sprite !== null && self.sprite.rotation <= 1.5708)
self.sprite.rotation += 0.03;
self.renderer.render(self.stage);
};
At the end of the Setup function I try to call the Rotate function and this is where it fails.
I have found that this code prevents the addChild problem that you had. Look at the comments in the code for explanation.
var Card = function(renderer, stage) {
var self = this;
self.name = "None";
self.health = 5;
self.renderer = renderer;
self.stage = stage;
self.sprite = null;
var x = function(){
self.Setup.apply(self,arguments);
}
PIXI.loader.add("FirePlace/GW2-Logo.jpg").load(x); // origionally: PIXI.loader.add("FirePlace/GW2-Logo.jpg").load(self.Setup);
// self would not be defined in whatever other context it is used in when it is called. All I had to do was wrap it in a function and it was fixed
};
Card.prototype.Setup = function() {
var self = this;
self.sprite = new PIXI.Sprite(PIXI.loader.resources["FirePlace/GW2-Logo.jpg"].texture);
console.log("Stage is:",self.stage); // Now stage is defined since I wrapped this function in another function.
self.stage.addChild(self.sprite);
console.log("Sprite loaded");
self.renderer.render(self.stage);
self.Rotate();
};
Card.prototype.SetName = function(name) {
var self = this; // I also defined self = this in every single function :D
self.name = name;
};
Card.prototype.Rotate = function() {
var self = this;
var x = function(){
if (self.sprite === null)
console.log("Sprite is null");
if (self.sprite !== null && self.sprite.rotation <= 1.5708)
self.sprite.rotation += 0.03;
self.renderer.render(self.stage);
window.requestAnimationFrame(x);
}; // Same goes for this one.
window.requestAnimationFrame(x);
};
Within Card.prototype.Setup, and your other methods later added to your Card.prototype, self is window.self, because var is private and not part of the properties which your Constructor inherits from its prototype Object. Change self to this or Card (if you prefer the context to be bound) in there for the context you seek.

How can I refresh or load JSON to my viewModel on Knockout JS with complex models

I fork the code from here:
http://kindohm.github.io/knockout-query-builder/
The code works nice on the client side.
But when I try to save the viewModel as JSON and then retrieve the data from the server the UI never refresh at all.
This is the original viewModel:
window.QueryBuilder = (function(exports, ko){
var Group = exports.Group;
function ViewModel() {
var self = this;
self.group = ko.observable(new Group());
// the text() function is just an example to show output
self.text = ko.computed(function(){
return self.group().text();
});
}
exports.ViewModel = ViewModel;
return exports;
})(window.QueryBuilder || {}, window.ko);
I be added the next method to the viewModel
self.Save = function () {
console.log(ko.toJSON(self));
}
Added this button to the view
<input type="submit" value="Save" data-bind="click: Save"/>
This is the Group viewModel:
window.QueryBuilder = (function(exports, ko){
var Condition = exports.Condition;
function Group(data){
var self = this;
self.templateName = data.templateName;
self.children = ko.observableArray(data.children);
self.logicalOperators = ko.observableArray(data.logicalOperators);
self.selectedLogicalOperator = ko.observable(data.selectedLogicalOperator);
// give the group a single default condition
self.children.push(new Condition());
self.addCondition = function(){
self.children.push(new Condition());
};
self.addGroup = function(){
self.children.push(new Group());
};
self.removeChild = function(child){
self.children.remove(child);
};
// the text() function is just an example to show output
self.text = ko.computed(function(){
var result = '(';
var op = '';
for (var i = 0; i < self.children().length; i++){
var child = self.children()[i];
console.log(child);
result += op + child.text();
op = ' ' + self.selectedLogicalOperator() + ' ';
}
return result += ')';
});
}
exports.Group = Group;
return exports;
})(window.QueryBuilder || {}, window.ko);
So when I press the "save" button the console show the JSON from this viewModel, everything fine here.
This is the JSON returned:
{"group":{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0)"},"text":"(Points = 0 AND Points = 0 AND Points = 0)"}
I make a simple hack to avoid the connection to the server, so I take that json copy and paste on the load event and send to the constructor of the viewModel:
var vm;
window.addEventListener('load', function(){
var json = {"group":{"templateName":"group-template","children":[{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"},{"templateName":"condition-template","fields":["Points","Goals","Assists","Shots","Shot%","PPG","SHG","Penalty Mins"],"selectedField":"Points","comparisons":["=","<>","<","<=",">",">="],"selectedComparison":"=","value":0,"text":"Points = 0"}],"logicalOperators":["AND","OR"],"selectedLogicalOperator":"AND","text":"(Points = 0 AND Points = 0 AND Points = 0)"},"text":"(Points = 0 AND Points = 0 AND Points = 0)"};
vm = new QueryBuilder.ViewModel(json);
ko.applyBindings(vm);
}, true);
Then I modify the viewModel to recibe the json parameter
window.QueryBuilder = (function(exports, ko){
var Group = exports.Group;
function ViewModel(json) {
var self = this;
self.group = ko.observable(json.group);
// the text() function is just an example to show output
self.text = ko.computed(function(){
return self.group().text();
});
self.Save = function () {
console.log(ko.toJSON(self));
}
}
exports.ViewModel = ViewModel;
return exports;
})(window.QueryBuilder || {}, window.ko);
When I refresh the index.html the view is never loaded correctly and show this error on the JS console:
TypeError: self.group(...).text is not a function
return self.group().text();
Someone knows where is my mistake?
The last problem I had was related to the text() function on the child.
I fix this with the use of try/catch. So when the viewModel are new it have the text() function, but when this is loadad the text() does not exist, so I take the value directly from the "text" field.
try {
result += op + child.text();
}
catch(err) {
result += op + child.text;
}
The problem was on the Group class and Condition class.
This is the current and working code:
window.QueryBuilder = (function(exports, ko){
var Condition = exports.Condition;
function Group(data){
var self = this;
self.templateName = data.templateName;
self.children = ko.observableArray(data.children);
self.logicalOperators = ko.observableArray(data.logicalOperators);
self.selectedLogicalOperator = ko.observable(data.selectedLogicalOperator);
// give the group a single default condition
self.children.push(new Condition(data.children[0]));
self.addCondition = function(){
self.children.push(new Condition());
};
self.addGroup = function(){
self.children.push(new Group());
};
self.removeChild = function(child){
self.children.remove(child);
};
// the text() function is just an example to show output
self.text = ko.computed(function(){
var result = '(';
var op = '';
for (var i = 0; i < self.children().length; i++){
var child = self.children()[i];
try {
result += op + child.text();
}
catch(err) {
result += op + child.text;
}
op = ' ' + self.selectedLogicalOperator() + ' ';
}
return result += ')';
});
}
exports.Group = Group;
return exports;
})(window.QueryBuilder || {}, window.ko);
window.QueryBuilder = (function(exports, ko){
function Condition(data){
var self = this;
self.templateName = data.templateName;
self.fields = ko.observableArray(data.fields);
self.selectedField = ko.observable(data.selectedField);
self.comparisons = ko.observableArray(data.comparisons);
self.selectedComparison = ko.observable(data.selectedComparison);
self.value = ko.observable(data.value);
// the text() function is just an example to show output
self.text = ko.computed(function(){
return self.selectedField() +
' ' +
self.selectedComparison() +
' ' +
self.value();
});
}
exports.Condition = Condition;
return exports;
})(window.QueryBuilder || {}, window.ko);
Instead of self.group = ko.observable(json.group);, you should take a similar approach as you did on load self.group = ko.observable(new Group());, but this time pass the json.group data in Group
self.group = ko.observable(new Group(json.group));
I don't see where Group is defined, but you should make sure that it is able to handle and convert the JSON you now pass in, into observables.

javascript access "this" in function constructor

I'm trying to create a function constructor:
var obj = function() {
this.num = 2;
this.func = function() {
// need to access the **instance** num variable here
};
};
var instance = new obj();
I need to access the instance properties from a propery (which is the function func) of the object. But it doesn't work, since this is always the current function..
Store this in a variable which func can access:
var obj = function() {
var _this = this;
_this.num = 2;
_this.func = function() {
console.log(_this.num);
};
};
Please, use well-known approach, store this into separate field:
var obj = function() {
self = this;
self.num = 2;
self.func = function() {
alert(self.num);
// need to access the **instance** num variable here
};
};
var instance = new obj();
This is the pattern I use for the problem:
var obj = function(){
var self = this;
this.num = 2;
this.func = function() {
console.info(self.num);
};
};
var instance = new obj();
The variable self now can be accessed in all function of obj and is always the obj itself.
This is the same then:
var obj = function(){
var self = this;
self.num = 2;
self.func = function() {
console.info(self.num);
};
};
var instance = new obj();
You can do it using the Custom Constructor Functions, used to create a custom constructor and it's accessed without any problem, try it:
var Obj = function () {
this.num = 2;
this.func = function () {
alert("I have " + this.num);
return "I have " + this.num;
};
};
var instance= new Obj();
instance.func();//will return and show I have 2

How to iterate anonymous function inside each function in Knockout viewmodel

I am building a Knockout viewmodel. The model has some fields like dateFrom, DateTo, Status and so forth. In addition, there is a list of invoices.
The invoices have some pricing information, which is a price object. My main object also have a price object, which should iterate all the invoice objects and find the total price.
My problem is the following:
The code runs smooth, until I add the following in my view:
<label data-bind="text:totalPrice().price().priceExVat"></label>
Here I get an:
TypeError: $(...).price is not a function
Which refers to my:
exVat += $(ele).price().priceExVat;
I don't understand it, because in my each function, I should have the element. The element have a price() function, so why would it not work? Is it some scope issue?
My viewmodel:
function invoice(invoiceDate, customerName, pdfLink, status) {
var self = this;
self.pdfLink = pdfLink;
self.print = ko.observable(0);
self.customerName = customerName;
self.status = status;
self.pdfPagesCount = function () {
return 1;
};
self.invoiceDate = invoiceDate;
self.price = function () {
return new price(1.8, 2.1);
};
}
function price(exVat, total) {
var self = this;
self.currency = '€';
self.total = total;
self.priceExVat = exVat;
self.vatPercentage = 0.25;
self.vatAmount = self.exVat - self.total;
self.priceExVatText = function() {
return self.priceExVat + ' ' + self.currency;
};
}
var EconomicsViewModel = function (formSelector, data) {
var self = this;
self.dateFrom = data.dateFrom;
self.dateTo = data.dateTo;
self.invoices = ko.observableArray([
new invoice('05-05-2014', 'LetterAmazer IvS', "http://www.google.com","not printed"),
new invoice('05-05-2014', 'LetterAmazer IvS', "http://www.google.com", "not printed")
]);
self.totalPrice = function () {
var exVat = 0.0;
$(self.invoices).each(function (index, ele) {
console.log(ele);
exVat += $(ele).price().priceExVat;
});
return price(exVat, 0);
};
};
From what I read, totalPrice is actually a price object, you don't need to put a .price():
<label data-bind="text:totalPrice().priceExVat"></label>
EDIT:
Sorry, there were also problems on your javascript:
self.totalPrice = function () {
var exVat = 0.0;
$(self.invoices()).each(function (index, ele) { //<-- add () to self.invoices to get the array
console.log(ele);
exVat += ele.price().priceExVat; //<-- remove useless jQuery
});
return new price(exVat, 0); //<-- add 'new'
};
Check this fiddle
EDIT2:
To answer robert.westerlund's comment, you could remove $().each and replace with ko.utils.arrayForEach or even simpler use a for loop:
var arr = self.invoices();
for (var i = 0; i < arr.length; i++) {
console.log(arr[i]);
exVat += arr[i].price().priceExVat;
}
Updated fiddle

Categories

Resources