JavaScript/jQuery variable scope in class (this) - javascript

I'm writing simple slider for my website. This slider contains list items. I want to use OOP approach.
My actual code:
var miniSlider = function(objId)
{
this.obj = $("#" + objId);
this.obj.settings = [];
this.obj.settings['items'] = $('ul li', this.obj).length;
this.pagerNext = this.obj.find("i.next");
this.pagerPrev = this.obj.find("i.prev");
this.pagerNext.on("click", function() {
alert(this.obj.settings['items']);
});
};
I can invoke a few other sliders (yes, that's why I introduced a class):
miniSlider("mini-slider");
The problem is that when I'm in jQuery this.pagerNext.on("click", function() { }); this is no longer my object but - it's become a clicked element. How can I access this.obj.settings after click in a well done way (and with multi sliders support)?
EDIT:
Here is a full code created with a cooperation with SOF community :)
var MiniSlider = function(objId)
{
this.obj = $("#" + objId);
this.obj.settings = {
items: $("ul li", this.obj).length,
autoChangeTime: 8000
};
this.obj.activeElement = null;
this.pagerNext = this.obj.find("i.next");
this.pagerPrev = this.obj.find("i.prev");
var self = this;
this.pagerNext.on("click", function() {
self.obj.activeElement = $('li.active', self.obj);
if(self.obj.settings.items > 0)
{
if(self.obj.activeElement.is(':last-child'))
{
$('li.active', self.obj).removeClass('active');
$('li', self.obj).first().addClass('active');
}
else
{
self.obj.activeElement.next().addClass('active').prev().removeClass('active');
}
}
});
this.pagerPrev.on("click", function()
{
self.obj.activeElement = $('li.active', self.obj);
if(self.obj.settings.items > 0)
{
if(self.obj.activeElement.is(':first-child'))
{
self.obj.activeElement.removeClass('active');
$('li', self.obj).last().addClass('active');
}
else
{
self.obj.activeElement.prev().addClass('active').next().removeClass('active');
}
}
});
this.obj.parent().on('mouseenter mouseleave', function(e) {
if (e.type == 'mouseenter')
{
$(this).addClass('stop');
}
else
{
$(this).removeClass('stop');
}
});
setInterval(function() {
if(self.obj.settings.items > 0 && !self.obj.parent().hasClass("stop"))
{
self.pagerNext.click();
}
}, this.obj.settings.autoChangeTime);
};
and invoke:
new MiniSlider("mini-slider");

Alex gave you the solution to the this problem in your callback, but there is another problem in your code.
You are calling the miniSlider() function without a new operator:
miniSlider("mini-slider");
That means that inside the function, this is not a unique object, but is actually the window object!
You need to use the new operator to create an individual object for each call:
new miniSlider("mini-slider");
But you should also change the name of this function to follow the JavaScript convention that constructors begin with a capital letter. Call it MiniSlider and use it like so:
new MiniSlider("mini-slider");
If you follow this convention (which most experienced JavaScript programmers do), it will help you remember when to use new. If the function begins with a capital letter, it's a constructor and you need to use new with it. Otherwise, you don't.
If you'd like to be able to use your constructor without new, that is also possible with a bit more code, e.g.:
function MiniSlider( objId ) {
if( this == window ) return new MiniSlider( objId );
// the rest of your constructor code goes here
}
But generally people don't bother with that and just use the initial capital letter on the constructor as a reminder to use new.
Also, as a suggestion, I like to use a meaningful name when I save this in a variable, and then I use that name consistently instead of using this at all. Doing it this way it might look like:
var miniSlider = function(objId) {
var slider = this;
slider.obj = $("#" + objId);
slider.obj.settings = [];
slider.obj.settings['items'] = $('ul li', slider.obj).length;
slider.pagerNext = slider.obj.find("i.next");
slider.pagerPrev = slider.obj.find("i.prev");
slider.pagerNext.on("click", function() {
alert(slider.obj.settings['items']);
});
};
Why do I prefer that approach over using this in most places and another variable like self where you need it? This way I don't have to remember which to use: I can always use slider in the code instead of this. (Of course you could use self or any other name; I just like to have a more meaningful name if I'm going to the trouble of making up a name at all.)
Another minor problem in the code is in these two statements:
slider.obj.settings = [];
slider.obj.settings['items'] = $('ul li', slider.obj).length;
You shouldn't use an Array when you are going to be giving it named properties like this. Use an Object instead. Arrays should only be used when you have numeric indexes like 0, 1, 2, etc. And with an object literal you can set the property at the same time:
slider.obj.settings = {
items: $('ul li', slider.obj).length
};
Also, when you use that property:
alert(slider.obj.settings['items']);
you can write it more simply as:
alert(slider.obj.settings.items);
Either way it does the same thing.

Save a reference to this in a local variable, and use that variable instead of this in the nested function.
var self = this;
this.pagerNext.on("click", function() {
alert(self.obj.settings['items']);
});

Related

How can I reuse an object literal

I'm trying to reuse an object I created to dynamically create more than one slider on a page.
My idea was to create an array and push my slider object there as often as needed, so I could access it by id. Unfortunatelly it doesn't work. Hope someone can point me in the right direction ...
So what I have is this;
var slider = {
"init":function(slide_it){
this.parent = $(slide_it);
/Count Elements and create a navigation depending on the count etc./
},
"otherstuff":{...}
}
In my (document).ready function I create an array and fill it up with different slider objects, add Ids to an accordion and call the init function:
var slide_array = [];
var accordion_sections = $('#accordion > div').length;
for(var i = 0; i < accordion_sections; i++){
slide_array.push(slider);
$('#accordion').children('div').eq(i).attr('id', 'slide_it_'+ i);
slide_array[i].init($('#slide_it_' + i).find('.slider'));
}
Then I have a button with class="next" and I call a function within the slider
$('.next').click(function(){
slide_array[0].otherstuff();
});
My plan is to get the parent of .next and its id so that I can use slide_array[parentID].otherstuff();
But ... it's not working propperly when I call the init function inside the for loop more then once.
More weird, some functions calls seem to work, other have no effect.
What am I doing wrong?
You can use Object.create.
var s1 = Object.create(slider),
s2 = Object.create(slider);
s1.init(...);
s2.init(...);
If you return this from init your will be able to chain like:
var s1 = Object.create(slider).init(...);
However at this point I would just ditch the object literal and use constructors, since this is what you need.
function Slider(slide_it) {
this.parent = $(slide_it);
}
Slider.prototype = {
constructor: Slider,
otherStuff: function () {}
};
var s1 = new Slider(...),
s2 = new Slider(...);
Write a function to return the object:
function slider() {
return {
"init":function(slide_it){
this.parent = $(slide_it);
/Count Elements and create a navigation depending on the count etc./
},
"otherstuff":{...}
};
}
Then:
slide_array.push( slider() );
That'll give you a separate object every time. In you're version, you're filling the array with references to the same single object.
Why not just turn that into a jQuery plugin ?
jQuery.fn.slider = function(options) {
return this.each(function() {
var sliderElem = $(this),
settings = $.extend({
speed : 3000,
something : 'other thing'
}, options);
otherStuff(sliderElem);
});
function otherStuff(elem) {
}
}
$('#accordion > div').slider();
No iteration or jumping through hoops, just call it on the collection and it creates a new slider for each element ?

Cannot call method 'find' of undefined

I'm trying to use the objects within my jQuery code.
I've nearly this:
var opts = {
ul: $(this).find('.carousel'),
li: ul.find('li')
}
li property gives an error Cannot call method 'find' of undefined
How can it be fixed?
It doesn't matter what your selector is, you can't access a property of an object that you are declaring, while you are declaring it.
Why would ul be declared? You're making a property ul on your opts object.
Ugly:
What you might want is:
var opts = {
ul: $(this).find('.carousel')
}
opts.li = opts.ul.find('li');
But if you don't actually need references to both groups then:
Cleaner:
var opts = {
li: $(this).find('.carousel li')
}
is just as good.
Cleanest:
You could also do:
var $carousel = $(this).find('.carousel');
var options = {
carousel: $carousel,
carouselItems: $carousel.find('li')
}
Godawful, but you asked for it:
var CarouselOptions = (function () {
var options = function (carousel) {
this.carousel = $(carousel);
this.carouselItems = this.carousel.find('li');
};
options.prototype.myOptionsFunction = function () {
// Don't know what you want your object to do, but you wanted a prototype...
};
return options;
})();
var opts = new CarouselOptions($(this).find('.carousel'));
Also
(Be careful with what your this is, presumably you have more than one .carousel element on the page, and here you want the one that is within the target of an event.)
Your error message is essentially saying that $(this) is undefined (or in other words, jQuery couldn't find this element). Because you don't have any code other than the single object you are trying to set, I don't know what the actual value of this is.
What I would do is ensure that this is set to an element of some sort. A simple console.log(this) should handle that. If this isn't an HTML element, then that's your problem. Either ensure you are inside of a jQuery event function like this:
$('#id').click(function() {
this === document.getElementById('id'); // true
});`
Or you can just drop the $(this):
var opts = {};
opts.ul = $('.carousel'),
opts.li = opts.ul.find('li')
var that = $(this);
var opts = {
ul: that.find('.carousel'),
li: ul.find('li')
}

Creating a simple JavaScript class with jQuery

I'm trying to understand jQuery classes but it is not going very well.
My goal is to use a class this way (or to learn a better way to do it):
var player = new Player($("playerElement"));
player.InitEvents();
Using other people's examples, this is what I tried:
$.Player = function ($) {
};
$.Player.prototype.InitEvents = function () {
$(this).keypress(function (e) {
var key = e.which;
if (key == 100) {
MoveRight();
}
if (key == 97) {
MoveLeft();
}
});
};
$.Player.prototype.MoveRight = function () {
$(this).css("right", this.playerX += 10);
}
$.Player.prototype.MoveLeft = function () {
$(this).css("right", this.playerX -= 10);
}
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};
The end goal is to have a character moving on the screen left and right using the keyboard letters A and D.
I have a feeling that I'm doing something very wrong with this "class"
but I'm not sure why.
(sorry for my English)
An important issue is that you have to assign the passed jQuery object/element to a this.element - or another this.propertyName - so you can access it later inside the instance's methods.
You also cannot call MoveRight()/MoveLeft() directly like that because those functions are not defined up in the scope chain, but rather in the prototype of your instance's Constructor, hence you need a reference to the instance itself to call these.
Updated and commented code below:
(function ($) { //an IIFE so safely alias jQuery to $
$.Player = function (element) { //renamed arg for readability
//stores the passed element as a property of the created instance.
//This way we can access it later
this.element = (element instanceof $) ? element : $(element);
//instanceof is an extremely simple method to handle passed jQuery objects,
//DOM elements and selector strings.
//This one doesn't check if the passed element is valid
//nor if a passed selector string matches any elements.
};
//assigning an object literal to the prototype is a shorter syntax
//than assigning one property at a time
$.Player.prototype = {
InitEvents: function () {
//`this` references the instance object inside of an instace's method,
//however `this` is set to reference a DOM element inside jQuery event
//handler functions' scope. So we take advantage of JS's lexical scope
//and assign the `this` reference to another variable that we can access
//inside the jQuery handlers
var that = this;
//I'm using `document` instead of `this` so it will catch arrow keys
//on the whole document and not just when the element is focused.
//Also, Firefox doesn't fire the keypress event for non-printable
//characters so we use a keydown handler
$(document).keydown(function (e) {
var key = e.which;
if (key == 39) {
that.moveRight();
} else if (key == 37) {
that.moveLeft();
}
});
this.element.css({
//either absolute or relative position is necessary
//for the `left` property to have effect
position: 'absolute',
left: $.Player.defaultOptions.playerX
});
},
//renamed your method to start with lowercase, convention is to use
//Capitalized names for instanceables only
moveRight: function () {
this.element.css("left", '+=' + 10);
},
moveLeft: function () {
this.element.css("left", '-=' + 10);
}
};
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};
}(jQuery));
//so you can use it as:
var player = new $.Player($("#playerElement"));
player.InitEvents();
Fiddle
Also note that JavaScript does not have actual "classes" (at least not until ES6 gets implemented) nor Methods (which by definition are associated exclusively to Classes), but rather Constructors which provide a sweet syntax that resembles classes. Here's an awesome article written by TJ Crowder regarding JS's "fake" methods, it is a little advanced but everyone should be able to learn something new from reading it:
http://blog.niftysnippets.org/2008/03/mythical-methods.html
When you use this inside your Player prototype functions, this points to the current Player object.
But when you use $(this).keypress it requires that this points to an HTML element.
The two simply are incompatible. There is only one this and it points to the current Player object, not to an HTML element.
To fix your problem, you will need to pass the HTML element into the Player object upon its creation or into the relevant function calls.
You can pass the element into the Player object upon construction like this:
$.Player = function ($, element) {
this.element = element;
};
$.Player.prototype.InitEvents = function () {
$(this.element).keypress(function (e) {
var key = e.which;
if (key == 100) {
MoveRight();
}
if (key == 97) {
MoveLeft();
}
});
};
$.Player.prototype.MoveRight = function () {
$(this.element).css("right", this.playerX += 10);
}
$.Player.prototype.MoveLeft = function () {
$(this.element).css("right", this.playerX -= 10);
}
$.Player.defaultOptions = {
playerX: 0,
playerY: 0
};

Difficulty developing unique dynamic onclick events in Javascript

I am working with a decent sized set of data relating to objects on the page and some objects need links applied to them onclick. The link to connect to is part of the dataset and I build a string for the link with the variable linkTarget and apply it like so.
if (dataTag[i][3]==true){
if(prepend==undefined || prepend=="undefined"){
var linkTarget=ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
} else {
var linkTarget=prepend+ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
}
ele refers to an element picked up with getElementByID. Now I am going through quite a few objects and the problem I have is the onclick for every object is the last value of linkTarget. This is all contained in a function and link target is a local variable so I have no idea why. I have tried using an array with something like
ele.onclick=function(){window.open(linkTarget[linkTarget.length-1]);};
and even
ele.onclick=function(){window.open(linkTarget.valueOf());};
with the same results. I am at a loss now and would appreciate any help.
Use Array.forEach() to iterate your data and watch your troubles melt away.
dataTag.forEach(function (item) {
if (item[3]==true) {
var linkTarget = "";
if (prepend==undefined || prepend=="undefined") {
linkTarget = prepend;
}
linkTarget += ResultsJSON.targetUrl;
ele.onclick = function () {
window.open(linkTarget);
};
}
});
See this compatibility note for using Array.forEach() in older browsers.
You're in a loop — therefore, you need to put your things-to-be-executed in another function, like so:
if(dataTag[i][3]) {
if(prepend) {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
} else {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
}
I also made some general corrections.

Is there a Javascript equivalent of Ruby's andand?

In trying to make my Javascript unobtrusive, I'm using onLoads to add functionality to <input>s and such. With Dojo, this looks something like:
var coolInput = dojo.byId('cool_input');
if(coolInput) {
dojo.addOnLoad(function() {
coolInput.onkeyup = function() { ... };
});
}
Or, approximately equivalently:
dojo.addOnLoad(function() {
dojo.forEach(dojo.query('#cool_input'), function(elt) {
elt.onkeyup = function() { ... };
});
});
Has anyone written an implementation of Ruby's andand so that I could do the following?
dojo.addOnLoad(function() {
// the input's onkeyup is set iff the input exists
dojo.byId('cool_input').andand().onkeyup = function() { ... };
});
or
dojo.byId('cool_input').andand(function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
I don't know Dojo, but shouldn't your first example read
dojo.addOnLoad(function() {
var coolInput = dojo.byId('cool_input');
if(coolInput)
coolInput.onkeyup = function() { ... };
});
Otherwise, you might end up trying to access the element before the DOM has been built.
Back to your question: In JavaScript, I'd implement andand() as
function andand(obj, func, args) {
return obj && func.apply(obj, args || []);
}
Your example could then be written as
dojo.addOnLoad(function() {
andand(dojo.byId('cool_input'), function() {
this.onkeyup = function() { ... };
});
});
which isn't really that much shorter than using the explicit if statement - so why bother?
The exact syntax you want is not possible in JavaScript. The way JavaScript executes would need to change in a pretty fundamental fashion. For example:
var name = getUserById(id).andand().name;
// ^
// |-------------------------------
// if getUserById returns null, execution MUST stop here |
// otherwise, you'll get a "null is not an object" exception
However, JavaScript doesn't work that way. It simply doesn't.
The following line performs almost exactly what you want.
var name = (var user = getUserById(id)) ? user.name : null;
But readability won't scale to larger examples. For example:
// this is what you want to see
var initial = getUserById(id).andand().name.andand()[0];
// this is the best that JavaScript can do
var initial = (var name = (var user = getUserById(id)) ? user.name : null) ? name[0] : null;
And there is the side-effect of those unnecessary variables. I use those variables to avoid the double lookup. The variables are mucking up the context, and if that's a huge deal, you can use anonymous functions:
var name = (function() {return (var user = getUserById(id)) ? user.name : null;})();
Now, the user variable is cleaned-up properly, and everybody's happy. But wow! what a lot of typing! :)
You want dojo.behavior.
dojo.behavior.add({
'#cool_input': {
onKeyUp: function(evt) { ... }
}
});
How about something like this:
function andand(elt, f) {
if (elt)
return f(elt);
return null;
}
Call like this:
andand(dojo.byId('cool_input'), function(elt) {
// this function gets called with elt = the input iff it exists
dojo.addOnLoad(function() {
elt.onkeyup = function() { ... };
});
});
As far as I know there isn't a built-in JavaScript function that has that same functionality. I think the best solution though is to query by class instead of id and use dojo.forEach(...) as you will be guaranteed a non-null element in the forEach closure.
You could always use the JavaScript equivalent:
dojo.byId('cool_input') && dojo.byId('cool_input').whateverYouWantToDo(...);
I've never used dojo, but most javascript frameworks (when dealing with the DOM) return the calling element when a method is called from the element object (poor wording, sorry). So andand() would be implicit.
dojo.addOnLoad(function() {
dojo.byId('cool_input').onkeyup(function(evt) { /*event handler code*/
});
});
For a list:
Array.prototype.andand = function(property, fn) {
if (this.filter(property).length > 0) this.map(fn);
}

Categories