Javascript setters/getters - javascript

var author = {
firstname: 'Martin',
lastname: 'Hansen'
}
function settersGetters(propStr) {
for (var i = 0; i < propStr.length; i++) {
author['_'+ propStr[i]] = null;
author.__defineGetter__(propStr[i],
function() {
return author['_'+ propStr[i]];
});
author.__defineSetter__(propStr[i],
function(val) {
author['_'+ propStr[i]] = val;
});
};
}
The above code would hopefully generate setters/getters for any supplied properties (in an array) for the object author.
But when I call the below code Both firstname and lastname is olsen.. What am I doing wrong?
settersGetters(['firstname', 'lastname']);
author.firstname = 'per';
author.lastname = 'olsen';
console.log(author.firstname);
console.log(author.lastname);

I suspect this is a closure issue, which several helpful people explained to me here.
Try wrapping the i reference inside a function, and read up on closures. Despite all the help, I confess that I still don't really understand them.

The definition is made in a closure, so all the setters are using the last value of i.
Use this instead:
function setterGetter(property)
{
author['_'+ property] = null;
author.__defineGetter__(property,
function() {
return author['_'+ property];
});
author.__defineSetter__(property,
function(val) {
author['_'+ property] = val;
});
}
function settersGetters(propStr) {
for (var i = 0; i < propStr.length; i++) {
setterGetter(propStr[i]);
};
}

Related

How to connect a button function to an object property?

This is the simplified code I'm working with. Is there a better way to find the correct bool?
I'm a beginner so I dont understand keyword "this" completely, but is there a way to connect button with a coresponding object maybe with "this" or something simmilar?
arr = [
{
name: "obj1",
bool: false,
},
{
name: "obj2",
bool: false,
}
];
function buildButtons() {
for (var i = 0; i < arr.length; i++) {
let button = document.createElement("button");
button.innerHTML = arr[i].name;
button.onclick = function() {
// is there a better way to find the correct bool?
for (i = 0; i < arr.length; i++) {
if (this.innerHTML === arr[i].name) {
arr[i].bool = true;
}
}
};
window.document.body.appendChild(button);
}
}
buildButtons();
Your best bet is to change the data structure to a more appropriate one. Rather than having to loop through an array to find the correct name, just use the names as keys in a single object, so you can look them up directly. Altered code follows:
obj = { obj1: false, obj2: false}
function buildButtons() {
for (let name in obj) {
let button = document.createElement("button");
button.innerHTML = name;
button.onclick = function() {
obj[name] = true;
}
window.document.body.appendChild(button);
}
}
buildButtons();
Note that I've used let rather than var for the loop variable - this is necessary (in your version too) for the event handler functions to "capture" the correct value of the loop variable. See here for a very thorough explanation of this.

Why is a function returning undefined and how to debug it?

I'm experimenting with closures and classes in data variables and in the example below I'm getting undefined even though I placed a console.log() right before the function returns the result and it isn't undefined. It seems to work if it isn't attached to an event handler. Can someone tell me why is this happening and if there is a way to spot where exactly does the error happen? When debugging it goes from the console log straight to the error and I don't see how that makes sense.
To trigger the error run the snippet and click on the names.
The same functions in $('#Individuals').data('functions') can be chained and work fine when called in IndividualsList(), but not from the event listener, then the result becomes undefined.
$(document).ready(function() {
var thisWindow = $('#Individuals');
var randomNames = ['Sonia Small', 'Kurt Archer', 'Reese Mullins', 'Vikram Rayner', 'Jethro Kaye', 'Suhail Randolph', 'Kaydon Crouch', 'Jamaal Elliott', 'Herman Atkins', 'Sia Best', 'Kory Gentry', 'Fallon Sawyer', 'Zayyan Hughes', 'Ayomide Byers', 'Emilia Key', 'Jaxson Guerrero', 'Gracey Frazier', 'Millie Mora', 'Akshay Parker', 'Margareta Emiliana'];
var generatedIndividuals = [];
function generateIndividual(name) {
return {
IndividualName: name
};
}
function IndividualsList(element) {
var list = [];
this.add = function(thisIndividual) {
$('#Individuals').data('functions').init(element, list).add(thisIndividual);
}
this.refresh = function() {
$('#Individuals').data('functions').init(element, list).refresh();
}
this.sort = function(order) {
$('#Individuals').data('functions').init(element, list).sort(order);
}
}
thisWindow.data('functions', (function() {
var element = $();
var list = [];
return {
add: function(thisIndividual) {
list.push(thisIndividual);
return thisWindow.data('functions');
},
init: function(thisElement, thisList) {
element = thisElement;
list = thisList;
return thisWindow.data('functions');
},
refresh: function() {
var thisList = element.html('');
for (let i = 0; i < list.length; i++) {
thisList.append(
'<div>' + list[i].IndividualName + '</div>'
);
}
return thisWindow.data('functions');
},
sort: function(order) {
list.sort(function(a, b) {
if (a.IndividualName < b.IndividualName) return -1 * order;
if (a.IndividualName > b.IndividualName) return 1 * order;
return 0;
});
console.log(thisWindow.data('functions'));
return thisWindow.data('functions');
}
}
})());
for (let i = 0; i < 20; i++) {
let nameNum = Math.floor(Math.random() * randomNames.length);
let thisClient = generateIndividual(randomNames[nameNum]);
generatedIndividuals.push(thisClient);
}
(function() {
var targetElement = thisWindow.find('div.individuals-list');
var targetData = {}
targetElement.data('individualsList', new IndividualsList(targetElement));
targetData = targetElement.data('individualsList');
for (let i = 0; i < generatedIndividuals.length; i++) {
targetData.add(generatedIndividuals[i]);
}
targetData.refresh();
})();
thisWindow.on('click', '.individuals-list', function() {
var thisElem = $(this);
var order = parseInt(thisElem.data('order'));
thisWindow.find('div.individuals-list').data('individualsList').sort(order).refresh();
thisElem.data('order', order * (-1));
});
});
.individuals-list {
border: 1px solid;
cursor: pointer;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="Individuals">
<div class="individuals-list" data-order="1"></div>
</div>
https://jsfiddle.net/Kethus/ymgwrLhj/
You are referring to the wrong sort() function, hence call it incorrectly so it returns undefined. Then you call refresh() on undefined that was returned from sort. Here's why:
In your IFFE, you use .data() to set the data = new IndvidualsList on thisWindow.find('div.individuals-list')
This code:
thisWindow.find('div.individuals-list').data('individualsList')
Returns that instantiated IndividualsList Object:
IndividualsList = $1
add: function(thisIndividual)
refresh: function()
sort: function(fieldName, order)
IndividualsList Prototype
Note the sort() function's definition. Sort in this object requires two parameters, fieldName and order; yet you call sort() and only pass order;
This indicates your expectation for the sort() function is incorrect or the wrong sort function is being made available at that line of code (in the click handler).
How to debug
Set a breakpoint at line 132 of the provided JavaScript in the
Fiddle.
Click a name in the list.
While at the breakpoint (execution paused), move to the console and run this in the console:
thisWindow.find('div.individuals-list').data('individualsList')
Note the sort() function definition in the list of functions
Next, in the console run this statement:
thisWindow.find('div.individuals-list').data('individualsList').sort(order)
Note the return is undefined <-- This is the issue
The returned value doesn't transfer from the closure to the instance that called it, the class has to be changed like so:
function IndividualsList(element) {
var list = [];
this.add = function(thisIndividual) {
return $('#Individuals').data('functions').init(element, list).add(thisIndividual);
}
this.refresh = function() {
return $('#Individuals').data('functions').init(element, list).refresh();
}
this.sort = function(order) {
return $('#Individuals').data('functions').init(element, list).sort(order);
}
}
The breakpoint could have been in one of IndividualsList()'s methods so it can be noticed that the closure returns the desired object while the method does not. Different names for either the functions or methods would help to reinforce that they are separate.

Linking property of a custom element to it's attribute

Question & Demo
I've recently started to work with custom elements.
As you know, a HTMLElement has both a markup inside the document, and a JavaScript object. So, with my custom element, I've tried to link the JavaScript object properties with the element's attributes.
So, if any of those is updated, the other would be updated as well. But this isn't happening and I swear I've tried everything, maybe is something stupid I'm missing but for me, how this code is behaving is a freaking mistery.
After reading the code explanation below and seen the demo, you should be able to understand my question:
Why are the custom element attributes updating correctly, but not it's properties?
I've setup a JSFiddle to illustrate my problem, and I will be going over how the code is supposed to work in this post.
HTML
<e-button color="red" width="250px">RED BUTTON</e-button>
Well it rarely gets any simpler than that. I create a custom object called "e-button", with color=red and width=250px.
JavaScript
var eButtonProto = Object.create(HTMLElement.prototype);
eButtonProto.createdCallback = function() {
this.__htmlToJsProp(); //Gets all the HTML attributes and makes them accessible via JS.
this.__processAttr(); //Makes decision upon predefined attributes.
}
eButtonProto.__htmlToJsProp = function() {
var attr = this.attributes;
for (var i = 0; i < attr.length; i++) {
var current = attr[i];
var name = current.name;
var value = current.value;
this[name] = value;
Object.defineProperty(this, name, {
get: function() {
return this.getAttribute(name);
},
set: function(val) {
this.setAttribute(name, val);
}
});
}
}
eButtonProto.attributeChangedCallback = function(name, oldVal, val) {
this[name] = val;
this.__processAttr();
}
eButtonProto.__processAttr = function() {
var color = this.color || this.defaults.color;
this.style.backgroundColor = color;
}
eButtonProto.defaults = {
color: "whitesmoke"
}
var eButton = document.registerElement("e-button", {
prototype: eButtonProto
});
window.onload = function() {
redButton = document.querySelector("e-button[color=red]");
console.log("button ATTRIBUTES", redButton.getAttribute("color"), redButton.getAttribute("width"));
console.log("button PROPERTIES", redButton.color, redButton.width);
} < /script>
The really important code snippets here are these, which essentialy should make my idea work, first, the __htmlToJsProp() function:
eButtonProto.__htmlToJsProp = function() {
var attr = this.attributes; //Gets the element's attributes.
for (var i = 0; i < attr.length; i++) {
var current = attr[i]; //Element attribute name,value pair.
var name = current.name; //Attribute name.
var value = current.value; //Attribute value.
Object.defineProperty(this, name, { //Defines the element property from the attribute name, for simplicity I will be using the color attribute as my example.
get: function() {
return this.getAttribute(name); //When accessing element.color you should get element.getAttribute("color")
},
set: function(val) {
this.setAttribute(name, val); //When setting element.color = "red" you should also be doing element.setAttribute("color","red");
}
});
this[name] = value; //Sets element.color = "red"
}
}
and then the attributeChangedCallback function:
eButtonProto.attributeChangedCallback = function(name, oldVal, val) {
this[name] = val; //This would be the other way around, if the attribute is updated via setAttribute, or the browser console, the property is updated (works).
this.__processAttr(); //You can ignore this
}
Conclusions
You see after testing A LOT I found that if you place yourself in the for loop and output the property value, it will give you element.color = "red" and element.width = "250px";
But if you test it outside the for loop, it gives you element.color = "250px" and element.width = "250px" for the properties but the attributes update properly, that is element.getAttribute("color") = "red" and element.getAttribute("width") = "250px".
If you made it this far, well thanks, hopefully you can find a way out of this problem, which I really don't seem to be able to solve, happy coding :)
Your issue seems to be within the for loop, the getters and setters are called later, so the value of i isn't what you think it is, the loop completes and sets i to the latest iterated value.
You'll solve it with a closure
eButtonProto.__htmlToJsProp = function () {
var attr = this.attributes;
for (var i = 0; i < attr.length; i++) {
(function(current, self) {
var name = current.name;
var value = current.value;
Object.defineProperty(self, name, {
get: function () {
return this.getAttribute(name);
},
set: function (val) {
this.setAttribute(name, val);
}
});
self[name] = value;
})(attr[i], this);
}
}
FIDDLE

Why is my variable undefined inside the Underscore.js each function?

Here is my code:
TextClass = function () {
this._textArr = {};
};
TextClass.prototype = {
SetTexts: function (texts) {
for (var i = 0; i < texts.length; i++) {
this._textArr[texts[i].Key] = texts[i].Value;
}
},
GetText: function (key) {
var value = this._textArr[key];
return String.IsNullOrEmpty(value) ? 'N/A' : value;
}
};
I'm using the Underscore.js library and would like to define my SetTexts function like this:
_.each(texts, function (text) {
this._textArr[text.Key] = text.Value;
});
but _textArr is undefined when I get into the loop.
In JavaScript, the function context, known as this, works rather differently.
You can solve this in two ways:
Use a temporary variable to store the context:
SetTexts: function (texts) {
var that = this;
_.each(texts, function (text) {
that._textArr[text.Key] = text.Value;
});
}
Use the third parameter to _.each() to pass the context:
SetTexts: function (texts) {
_.each(texts, function (text) {
this._textArr[text.Key] = text.Value;
}, this);
}
You have to pass this as context for _.each call like this:
_.each(texts, function (text) {
this._textArr[text.Key] = text.Value;
}, this);
See the docs for http://underscorejs.org/#each
this in javascript does not work the same way as you would expect. read this article:
http://www.digital-web.com/articles/scope_in_javascript/
short version:
the value of this changes every time you call a function. to fix, set another variable equal to this and reference that instead
TextClass = function () {
this._textArr = {};
};
TextClass.prototype = {
SetTexts: function (texts) {
var that = this;
for (var i = 0; i < texts.length; i++) {
that._textArr[texts[i].Key] = texts[i].Value;
}
},
GetText: function (key) {
var value = this._textArr[key];
return String.IsNullOrEmpty(value) ? 'N/A' : value;
}
};
Note that you can also pass things other that "this". For example, I do something like:
var layerGroupMasterData = [[0],[1,2,3],[4,5],[6,7,8,9],[10]];
_.each(layerGroupMasterData,function(layerGroup,groupNum){
_.each(layerGroup, function (layer, i) {
doSomethingThatComparesOneThingWithTheOverallGroup(layerGroupMasterData,layer);
},layerGroups);
},layerGroupMasterData);

How can I call any function in a chain of functions, without the chaining?

Sorry if my question wasn't clear enough. I'll put my code here...
var chain = {
'fn_1' : {
//fn_1 code here
chain.fn_2();},
'fn_2' : {
//fn_2 code here
chain.fn_3();}
...and so on
}
Let's say if i wana call chain.fn_1(), is there a way I can do that without calling chain.fn_2()?
What I can think of right now is a flag, but that would be alot of excess flags probably for each function. Do you guys have any ideas?
If the series of functions each call the next one you're correct, you'd need to have some sort of flag. In all likelihood, what would be best would be to modify your functions so that they return the reference to the object. Then you could chain like so:
var chain = {
'fn_1': function () {
// do something here.
return this;
},
'fn_2': function () {
// do something here.
return this;
},
'fn_3': function () {
// do something here.
return this;
}
};
// call the full chain:
chain.fn_1().fn_2().fn_3();
// call only the middle.
chain.fn_2();
g.d.d.c's answer is best, but if you can't modify the object for some reason, you could do this:
var _oldFn2 = chain.fn_2
chain.fn_2 = function() { return; };
chain.fn_1();
chain.fn_2 = _oldFn2;
var chain = {
fn : ['fn1', 'fn2', 'fn3'],
call : function(name) {
var i = 0, pos = -1, l = this.fn.length;
for(i = 0; i < l; i += 1) {
if(this.fn[i] == name) {
pos = i;
}
if(pos !== -1) {
this[this.fn[i]]();
}
}
},
fn1 : function() {
alert('fn1');
},
fn2 : function() {
alert('fn2');
},
};
chain.call('fn1'); //chain
chain.fn1(); //single

Categories

Resources