I'm building a class in js where I use jQuery.on() to do some stuff. I know that I can use bind to make the scope of the class is referred to this, but this way it replaces the scope of the current object inside the .on() function. I'm using the old trick of var self = this, and it works, but I'm wondering if there's a more elegant way to do that.
Here's an example of what I'm doing:
var MyClass = function(settings){
this.mySetting = settings.mySetting;
this.otherSetting = settings.otherSetting;
this._initFunction();
};
MyClass.prototype = {
mySetting : '',
otherSetting : '',
_initFunction: function(){
// keep a referente to the class scope
var self = this;
$('.selector').on( 'click', '.trigger', function(){
if( self.mySetting == 'something' && self.otherSetting = 'some other thing'){
// here, this is referred to '.trigger'
$( this ).slideUp();
}
});
}
}
But, if I do this, the code doesn't work because the scope issue:
var MyClass = function(settings){
this.mySetting = settings.mySetting;
this.otherSetting = settings.otherSetting;
this._initFunction();
};
MyClass.prototype = {
mySetting : '',
otherSetting : '',
_initFunction: function(){
$('.selector').on( 'click', '.trigger', function(){
if( this.mySetting == 'something' && this.otherSetting = 'some other thing'){
// here, this is referred to 'MyClass', so it won't work
$( this ).slideUp();
}
}.bind( this ) );
}
}
Any tips on how to make this more elegant, avoiding the use of var self = this?
One way would be to use bind to make this point to the MyClass instance and use the event object to get to DOM Elements that triggered the event.
MyClass.prototype = {
mySetting : '',
otherSetting : '',
_initFunction: function(){
$('.selector').on( 'click', '.trigger', function (event){
if( this.mySetting == 'something' && this.otherSetting = 'some other thing'){
$( event.target ).slideUp();
}
}.bind( this ) );
}
}
In addition to using bind, you can use jQuery's proxy functionality to do essentially the same thing, but without worrying about old browsers' lack of support for bind.
MyClass.prototype = {
mySetting : '',
otherSetting : '',
_initFunction: function(){
$('.selector').on( 'click', '.trigger', $.proxy(function (event){
if( this.mySetting == 'something' && this.otherSetting = 'some other thing'){
$( event.target ).slideUp();
}
}, this ) );
}
}
Related
The site Read-city uses the function function Popup (pop, name, html, handlers) in the file Read-city - online store of books_files\popup.js.Untitled, which creates a login/registration window (see https://ru.stackoverflow.com/questions/1194356/%d0%9e%d0%b1%d1%8a%d0%b5%d0%ba%d1%82-%d0%ba%d0%bb%d0%b0%d1%81%d1%81-popup )
function Popup (pop, name, html, handlers) {
if (html) {
$(document.body).append(html);
}
handlers = handlers || {};
this.$pop = $(pop);
this.selector = pop;
this.name = name;
this.$pop_wrapper = this.$pop.parent('.js__popup_main_wrapper');
Popup.instances[this.name] = this;
this.$pop.on('click', '.popup__close, .js__popup__close', this.hide.bind(this));
//Закрытие попапа при клике на маску и при нажатии Esc
if (this.$pop_wrapper) {
this.$pop_wrapper.on('click', function(event) {
var mask_wrapper = event.target;
if (mask_wrapper.classList.contains('js__popup_main_wrapper')) {
Popup.hideAll();
}
});
}
$(document).keyup(function(event) {
if (event.keyCode === 27) {
Popup.hideAll();
}
});
this.$body = this.$pop.find('.popup__body_text');
this.onshow = handlers.show || null;
this.onhide= handlers.hide || null;
this.events = {
closePopup: 'closePopup',
openPopup: 'openPopup'
}
}
Popup.instances = {};
Popup.getInstance = function(name, pop, html, handlers) {
if(Popup.instances[name]){
return Popup.instances[name];
}
if (pop){
return new Popup(pop, name, html, handlers);
}
return null;
};
Popup.hideAll = function() {
for (var popupName in Popup.instances) {
if(Popup.instances.hasOwnProperty(popupName)){
Popup.instances[popupName].hide();
}
}
};
Popup.prototype.show = function() {
Popup.hideAll();
//Скрываем скролл
$('body').css('overflow', 'hidden');
// временный костыль
// почему-то не всегда инициализируется dom-элемент попапа
if (!this.$pop.length) this.$pop = $(this.selector);
if (!this.$pop.length) return;
if (this.$pop_wrapper) {
this.$pop_wrapper.css("display", "flex");
}
this.$pop
.removeClass('hidden')
.addClass('shown')
.attr('data-opened', '');
this.onshow ? this.onshow() : null;
if (window.userCity && window.userCity.hideBlock) { //если открыт попап города(актуально для мобильной версии)
//то закрыть блок выбора города
window.userCity.hideBlock();
}
//создадим событие открытия попапа
eventEmitter.dispatch(this.events.openPopup, {
popupName: this.name,
popup: this.$pop
});
};
Popup.prototype.hide = function() {
this.$pop
.removeClass('shown')
.addClass('hidden')
.removeAttr('data-opened');
this.onhide ? this.onhide() : null;
if (this.$pop_wrapper) {
this.$pop_wrapper.fadeOut(50);
}
var opened = $('[data-opened]').length;
if (opened === 0) {
//Возвращаем скролл
$('body').css('overflow', 'auto');
}
eventEmitter.dispatch(this.events.closePopup, {
popupName: this.name,
popup: this.$pop
});
};
Popup.prototype.setBodyText = function (text){
if(!text){
return;
}
this.$body.html(text);
};
Explain
the meaning of the pop property
action of the operator this.$pop = $(pop)
and also the syntax of this.$pop.on ('click', '.popup__close, .js__popup__close', this.hide.bind (this)); is not clear.
1) the meaning of the pop property
It is whatever someone named a property to be. Sounds like it is the short name for pop up. But a variable name can be anything....
2) action of the operator this.$pop = $(pop)
It is taking a jQuery object return and puts it into a property. It is not uncommon for people to use $ in a variable name to denote it is a jQuery object and not a DOM reference.
3) and also the syntax of this.$pop.on ('click', '.popup__close, .js__popup__close', this.hide.bind (this)); is not clear.
It is the basic syntax of jQuery for attaching event listeners https://api.jquery.com/on
Very quickly, here is my take on this just looking at the code you provided.
I would say pop would be a html string selector / HTML element/ jQuery element.
That action should be creating an instance of a jQuery element. This element will help out interacting with pop HTML element under the jQuery "framework". A quick jQuery tutorial: https://www.w3schools.com/jquery/
It is part of the jQuery framework. Instead of addEventListener, it uses an on method to bind events. https://api.jquery.com/on/
Looking at the rest of the code, I'm pretty sure it is using jQuery.
I have been writing a plugin, and i really like this format
Function.prototype._onClick = function() {
// do something
}
Fuction.prototype.addListner = function() {
this.$element.on('click', this._onClick.bind(this));
}
the problem is sometimes i need the element being clicked and the main object. Doing as below i loose the dom element and not using bind looses the main object.
Fuction.prototype.addListner {
this.$element.find('.some-class').on('click', this._onClick.bind(this));
}
To achieve that i go back to ugly version
Fuction.prototype.addListner = function() {
var self = this;
this.$element.find('.some-class').on('click', function() {
self._onClick($(this));
});
}
Is there any better way to do this?
As zerkms, you can use the event.target to achieve what you want.
When using .on, the handler is :
handler
Type: Function( Event eventObject [, Anything extraParameter ] [, ...
] ) A function to execute when the event is triggered. The value false
is also allowed as a shorthand for a function that simply does return
false.
So your _onClick function will receive click event as its 1st parameter, then from event.target, you can now get the clicked item.
var Test = function(sel) {
this.$element = $(sel);
this.value = 'My value is ' + this.$element.data('val');
};
Test.prototype.addListner = function() {
this.$element.find('.some-class').on('click', this._onClick.bind(this));
}
Test.prototype._onClick = function(evt) {
// Get the target which is being clicked.
var $taget = $(evt.target);
//
console.log(this.value);
// use $target to get the clicke item.
console.log($taget.data('val'));
}
var test = new Test('#test');
test.addListner();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<div id="test" data-val="divVal">
<button class="some-class" data-val="Button-A">btnA</button>
<button class="some-class" data-val="Button-B">btnB</button>
</div>
hey guys a i downloaded some simple effect coded in JS . the plugin is called classie.js and the guy has written some custom code that interacts with this plugin classie.js
a similar question got asked a while ago classie.js Question and the guy really perfectly answered how the code inside classie.js is functioning . thats great , so now i understand how the code inside classie.js works , now my problem is , there is a lot of code written that actually interacts with this plugin called classie.js and and i have some difficulty understanding . so let me explain what elaboratly is my problem :
The classie.js code :
( function( window ) {
'use strict';
var hasClass, addClass, removeClass;
if ( 'classList' in document.documentElement ) {
// console.log(document.documentElement);
hasClass = function( elem, c ) {
// cons100%ole.log("elem is : " + elem + " c is " + c);
return elem.classList.contains( c );
};
addClass = function( elem, c ) {
console.log('elem Logged');
console.log(elem);
elem.classList.add( c );
};
removeClass = function( elem, c ) {
console.log('removeClass function got used in if statement')
elem.classList.remove
( c );
};
}
else {
// I have deleted this part as its only a fallback for older browsers. :)
}
function toggleClass( elem, c ) {
var fn = hasClass( elem, c ) ? removeClass : addClass;
fn( elem, c );
}
var classie = {
// full names
hasClass: hasClass,
addClass: addClass,
removeClass: removeClass,
toggleClass: toggleClass,
// short names
has: hasClass,
add: addClass,
remove: removeClass,
toggle: toggleClass
};
// transport
if ( typeof define === 'function' && define.amd ) {
// AMD
define( classie );
} else if ( typeof exports === 'object' ) {
// CommonJS
module.exports = classie;
} else {
// browser global
window.classie = classie;
}
})( window );
The code that Interacts with classie.js :
(function() {
// disable/enable scroll (mousewheel and keys) from https://stackoverflow.com/a/4770179
// left: 37, up: 38, right: 39, down: 40,
// spacebar: 32, pageup: 33, pagedown: 34, end: 35, home: 36
var keys = [32, 37, 38, 39, 40], wheelIter = 0;
function preventDefault(e) {
e = e || window.event;
if (e.preventDefault)
e.preventDefault();
e.returnValue = false;
}
function keydown(e) {
for (var i = keys.length; i--;) {
if (e.keyCode === keys[i]) {
preventDefault(e);
return;
}
}
}
function touchmove(e) {
preventDefault(e);
}
function wheel(e) {
// for IE
//if( ie ) {
//preventDefault(e);
//}
}
function disable_scroll() {
window.onmousewheel = document.onmousewheel = wheel;
document.onkeydown = keydown;
document.body.ontouchmove = touchmove;
}
function enable_scroll() {
window.onmousewheel = document.onmousewheel = document.onkeydown = document.body.ontouchmove = null;
}
var docElem = window.document.documentElement,
scrollVal,
isRevealed,
noscroll,
isAnimating,
container = document.getElementById( 'container' ),
trigger = container.querySelector( 'button.trigger' );
function scrollY() {
return window.pageYOffset || docElem.scrollTop;
}
function scrollPage() {
scrollVal = scrollY();
// console.log(scrollVal);
if( classie.has( container, 'notrans' ) ) {
classie.remove( container, 'notrans' );
return false;
}
if( isAnimating ) {
return false;
}
if( scrollVal <= 0 && isRevealed ) {
toggle(0);
}
else if( scrollVal > 0 && !isRevealed ){
toggle(1);
}
}
function toggle( reveal ) {
isAnimating = true;
if( reveal ) {
classie.add( container, 'modify' );
}
else {
noscroll = true;
disable_scroll();
classie.remove( container, 'modify' );
}
// simulating the end of the transition:
setTimeout( function() {
isRevealed = !isRevealed;
isAnimating = false;
if( reveal ) {
noscroll = false;
enable_scroll();
}
}, 600 );
}
// refreshing the page...
var pageScroll = scrollY();
noscroll = pageScroll === 0;
disable_scroll();
if( pageScroll ) {
isRevealed = true;
classie.add( container, 'notrans' );
classie.add( container, 'modify' );
}
window.addEventListener( 'scroll', scrollPage );
// trigger.addEventListener( 'click', function() { toggle( 'reveal' ); } );
})();
alot of the code that interacts with classie.js is actually derived from a thread directly from stackoverflow : how to disable and enable scroll
now all the above is just for your clear understanding , what my question really is , is i don't quite understand the usage of the add method in the code that interacts with the classie.js API , its somehow does't make any sense to me and MDN doc's says very little about this method . what is that method really really doing ?? .
Edit :: The confusing part :
function toggle( reveal ) {
isAnimating = true;
if( reveal ) {
classie.add( container, 'modify' );
}
else {
noscroll = true;
disable_scroll();
classie.remove( container, 'modify' );
}
// simulating the end of the transition:
setTimeout( function() {
isRevealed = !isRevealed;
isAnimating = false;
if( reveal ) {
noscroll = false;
enable_scroll();
}
}, 600 );
}
The above is the part that confuses me , am i right when i am guessing , that if any function from classie.js gotta be used , it has to be used like follows :
classie.functionname(); ?? and can't be directly assessed ?? eg. functionname();
My Second Big Problem (understanding JS syntax of classie.js) :
also as a supplementary question , you can choose not to answer , but certain parts of the code that interacts with classie.js has a lot of confusing syntax , let me point it out .
in the disable_scroll function there is this :
window.onmousewheel = document.onmousewheel = wheel;
document.onkeydown = keydown;
and in the enable scroll function there is this :
window.onmousewheel = document.onmousewheel = document.onkeydown = null;
now i understand
A = B ;
where ur assigning A the value of B ;
But The above Syntex is more like , A = B = C ; and thats totally over my head .
can somebody clarify Please
if somebody can be elaborate and explain , it would be great .
Thank you.
Alexander.
Don't have enough rep to comment yet.
The add() method is not a 'native' js function. If you look at the classie.js code, towards the end of it the is an object 'classie' which defines public shortcuts to the internal function addClass :
var classie = {
// full names
hasClass: hasClass,
addClass: addClass,
removeClass: removeClass,
toggleClass: toggleClass,
// short names
has: hasClass,
add: addClass,
remove: removeClass,
toggle: toggleClass
};
These shorcuts will let you call the internal functions (which cannot be accessed otherwise from the global scope) by calling classie.publicFunctionName(args) or window.classie.publicFunctionName(args) where "publicFunctionName" is the shorcut defined, which is exactly what the second chunk of code does :
...
classie.remove( container, 'modify' );
...
classie.add( container, 'modify' );
All the addClass() method does is add a class to the html element it is called on.
I believe this is called the 'revealing module' design pattern, but not 100% sure.
Hope that helps at least a bit.
If you want to learn a bit on js design patterns I warmly recommend reading Addy Osmani's very good (and free) book here : http://addyosmani.com/resources/essentialjsdesignpatterns/book/
In the following implementation of a hypothetical navigation module the module object returns properties such as isOverBinded or isNavTurnedOff which basically return the consequential value of other methods.
This methods are then utilised in the test cases to check whether a property call has caused its expected consequence.
Should these methods be kept or the original method in question return the consequential values and the same method to be used in the test case?
Currently the code is:
var navModule = (function(element) {
var nav = {};
var navHTMLobjs = {
navList : element,
listItems : element.find('li'),
listLinks : element.find('a')
};
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
};
nav.isOverBinded = function(){
return navHTMLobjs.navList.data('events').hasOwnProperty('mouseover')
&& navHTMLobjs.navList.data('events').hasOwnProperty('mouseout');
};
nav.turnOff = function() {
navHTMLobjs.navList.off('mouseover mouseout');
};
nav.isNavTurnedOff = function() {
return !navHTMLobjs.navList.data.hasOwnProperty('events');
};
nav.init = function() {
this.bindOver();
};
return nav;
});
var myNav = new navModule($('#nav'));
/// Test cases:
module('Navigation module');
test('Binding total', function() {
myNav.init();
equal(myNav.isOverBinded(), true, "Does the init function attach all events?");
});
test('Unbinding total', function() {
myNav.turnOff();
equal(myNav.isNavTurnedOff(), true, "Does the cancel function correctly unbind events?");
});
For example should I change nav.bingOver to be:
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
return navHTMLobjs.navList.data('events').hasOwnProperty('mouseover')
&& navHTMLobjs.navList.data('events').hasOwnProperty('mouseout');
};
...and then use the same method in the test case like below?
test('Binding total', function() {
myNav.init();
equal(myNav.bindOver(), true, "Does the init function attach all events?");
});
What are the differences between the two?
Many thanks
Assuming other parts of the app don't need to independently verify whether the events have been subscribed to, the bindOver() should not return any value. Also, the isOverBinded() doesnt belong to the navigation module. Its existence is purely to help implement the test. In such a case, that function should be within the testing suite.
var navModule = (function(element) {
var nav = {};
var navHTMLobjs = {
navList : element,
listItems : element.find('li'),
listLinks : element.find('a')
};
nav.bindOver = function() {
navHTMLobjs.navList.on('mouseover mouseout', 'li a', function(e) {
if (e.type == 'mouseover') {
$(this).addClass('over');
}
if (e.type == 'mouseout') {
$(this).removeClass('over');
}
});
};
nav.turnOff = function() {
navHTMLobjs.navList.off('mouseover mouseout');
};
nav.init = function() {
this.bindOver();
};
return nav;
});
//var myNav = new navModule($('#nav'));
/// Test cases:
module('Navigation module');
// you might already have such a in memory object
$root = $('<ul></ul>').append('<li></li><li></li>');
var myNav = new navModule($root);
test('Binding total', function() {
myNav.init();
equal(isOverBinded(), true, "Does the init function attach all events?");
});
test('Unbinding total', function() {
myNav.turnOff();
equal(isNavTurnedOff(), true, "Does the cancel function correctly unbind events?");
});
var isNavTurnedOff = function() {
return $root.data('events').hasOwnProperty('mouseover') && $root.data('events').hasOwnProperty('mouseout');
}
var isOverBinded = function() {
return $root.data.hasOwnProperty('events') === false;
}
At the end of the day I feel, whether or not the function ought to return a value should depend on the usage of the function and not for making testing easier.
Given the jQuery dropdown plugin below. Is there a way to add a method that would allow for a separate function outside of the dropdown to 'hideMenu'? Thanks
For example, if I applied the plugin to a div with an ID like so:
$('#settings.dropdown').dropDownMenu();
How could I then call to close the dropDownMenu w hideMenu from outside of the plugin? Thanks
jQuery.fn.dropDownMenu = function() {
// Apply the Dropdown
return this.each(function() {
var dropdown = $(this),
menu = dropdown.next('div.dropdown-menu'),
parent = dropdown.parent();
// For keeping track of what's "open"
var activeClass = 'dropdown-active',
showingDropdown = false,
showingMenu,
showingParent,
opening;
// Dropdown Click to Open
dropdown.click(function(e) {
opening = true; // Track opening so that the body click doesn't close. This allows other js views to bind to the click
e.preventDefault();
if (showingDropdown) {
dropdown.removeClass(activeClass);
parent.removeClass(activeClass);
showingMenu.hide();
showingDropdown = false;
} else {
showingDropdown = true;
showingMenu = menu;
showingParent = parent;
menu.show();
dropdown.addClass(activeClass);
parent.addClass(activeClass);
}
});
// When you click anywhere on the page, we detect if we need to blur the Dropdown Menu
$('body').click(function(e) {
if (!opening && showingParent) {
var parentElement = showingParent[0];
if (!$.contains(parentElement, e.target) || !parentElement == e.target) {
hideMenu();
}
}
opening = false;
});
// hides the current menu
var hideMenu = function() {
if(showingDropdown) {
showingDropdown = false;
dropdown.removeClass(activeClass);
parent.removeClass(activeClass);
showingMenu.hide();
}
};
});
};
jQuery advises making multiple methods available through the plugin itself:
jQuery.fn.dropDownMenu = function(method) {
var methods = {
init: function() {
// Put all your init code here
},
hide: function() {
hideMenu();
}
};
if ( methods[method] ) {
return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ));
} else if ( typeof method === 'object' || ! method ) {
return methods.init.apply( this, arguments );
} else {
$.error( 'Method ' + method + ' does not exist on jQuery.tooltip' );
}
function hideMenu() {
// ...
}
};
See http://docs.jquery.com/Plugins/Authoring#Plugin_Methods
Update: Use like this:
// Use the plugin normally to run the init method
$('#settings.dropdown').dropDownMenu();
// Call the hide method
$('#settings.dropdown').dropDownMenu('hide');
Sure. Give hideMenu to the global window object, like this:
window["hideMenu"] = function() {
if(showingDropdown) {
showingDropdown = false;
dropdown.removeClass(activeClass);
parent.removeClass(activeClass);
showingMenu.hide();
}
};
You can then call it as usual anywhere you need to.