Modularize/Objectify JQuery - javascript

I have a number of different "control elements" on my application: dropdowns, tabs, menus, etc. On same pages, there are many of the same control. When writing JavaScript to handle the different events associated with each of these controls, I'm trying to make my code as DRY as possible. One of the challenges is modularizing my JQuery code so that events that occur within a specific control only effect that control.
Take this initial code for example, all it does is open a dropdown menu when it is clicked. I'm used to writing just a ton of different anonymous functions triggered by different events so this type of JQuery is really new to me.
var dropdown = {
init: function() {
$(".dropdown").click(".dropdown", dropdown.openDropdown);
},
openDropdown: function() {
$(this).children(".dropdown-menu").show();
$(this).addClass("open");
}
}
$(document).ready(dropdown.init);
My question is, within this dropdown variable, I want to be able to save/track different pieces of the dropdown control currently being acted upon. For example, I might want to write:
var menu = $(this).children(".dropdown-menu");
somewhere in this chunk so that I could refer back to this menu while calling different functions. I just cannot figure out syntactically how to do this. Any help/guidance is welcomed! Thanks.

Something I like about coffeescript is how it allows you to easily create classes. Classes in coffee are just a simplified way of generating "modules" using javascript's prototypal inheritance. More on that here: http://coffeescript.org/#classes
But how YOU could implement more modular jQuery code is by doing something like this:
http://jsfiddle.net/x858q/2/
var DropDown = (function(){
// constructor
function DropDown(el){
this.el = $(el);
this.link = this.el.find("a");
this.menu = this.el.find(".dropdown-menu");
this.bindClick();
}
// method binding click event listener
DropDown.prototype.bindClick = function(){
var _this = this;
this.link.click(function(e){
_this.openDropDown();
e.preventDefault();
});
};
// click event handler
DropDown.prototype.openDropDown = function(){
this.menu.show();
this.link.addClass("open");
};
return DropDown;
})();
$(function(){
// init each .dropdown element as a new DropDown
$(".dropdown").each(function(){
new DropDown(this);
});
});

You've touched on a pattern I've been leaning towards more and more. Basically, create a JavaScript object that acts as a controller given a root element on the page. Since this "dropdown" is pretty generic, it could probably have access to the whole page and be perfectly happy. I would also recommend making these "modules" instantiable objects, as this allows you to write unit tests easier:
function DropdownModule() {
this.handleClick = this.handleClick.bind(this);
}
DropdownModule.prototype = {
element: null,
$element: null
constructor: DropdownModule,
init: function(element) {
this.setElement(element);
this.$element.on("click", ".dropdown", this.handleClick);
},
handleClick: function(event) {
var $dropdown = $(event.currentTarget);
$dropdown.children(".dropdown-menu").show();
$dropdown.addClass("open");
this.someOtherFunction($dropdown);
},
someOtherFunction($dropdown) {
// do something with $dropdown
},
setElement: function(element) {
this.element = element;
this.$element = $(element);
}
}
Then to use it, just throw this anywhere after the definition for Dropdown:
var dropdown = new Dropdown()
.init(document.documentElement);
The document.documentElement property refers to the <html> tag and is available the moment JavaScript begins executing.
As a side note, I've built a whole framework around this approach: Foundry. Other frameworks, like Angular, take a similar approach as well.

What you want sounds like exactly what jQuery UI has already implemented in their Widget Factory.
I'd highly recommend you check it out since what you'd end up with it something like
$.widget( 'dropdown', {
_create: function() {
this.element.addClass( 'dropdown' );
this._on({
'click': '_clicked'
});
},
_clicked: function( event ) {
// `this` is an instance of dropdown here, not the element
this.clicked = !this.clicked;
this.element.toggleClass( 'clicked', this.clicked );
},
_destroy: function() {
this.element.removeClass( 'dropdown' );
}
});
Then you would use it like any other jQuery UI Widget
$( '#some-element' ).dropdown();

Related

jQuery when element becomes visible [duplicate]

This question already has answers here:
How to check if element is visible after scrolling?
(46 answers)
Closed 1 year ago.
Basically, I am wondering if there is a way to automatically run a function when an element becomes hidden or visible, not on a user click but automatically in another script.
I don't want this to just run one time, because the elements (such as a slider) constantly change from visible to hidden.
Would this be something that jQuery can do with bind? Such as binding the element's visibility to a function (I don't know how to write this)
If you need me to elaborate more on what I'm trying to do, let me know. Thanks
Pseudocode:
$('#element').bind('display:none', function);
function(){
//do something when element is display:none
}
$('#element').bind('display:block', function2);
function2(){
//do opposite of function
}
There are no events in JQuery to detect css changes.
Refer here: onHide() type event in jQuery
It is possible:
DOM L2 Events module defines mutation events; one of them - DOMAttrModified is the one you need. Granted, these are not widely implemented, but are supported in at least Gecko and Opera browsers.
Source: Event detect when css property changed using Jquery
Without events, you can use setInterval function, like this:
var maxTime = 5000, // 5 seconds
startTime = Date.now();
var interval = setInterval(function () {
if ($('#element').is(':visible')) {
// visible, do something
clearInterval(interval);
} else {
// still hidden
if (Date.now() - startTime > maxTime) {
// hidden even after 'maxTime'. stop checking.
clearInterval(interval);
}
}
},
100 // 0.1 second (wait time between checks)
);
Note that using setInterval this way, for keeping a watch, may affect your page's performance.
7th July 2018:
Since this answer is getting some visibility and up-votes recently, here is additional update on detecting css changes:
Mutation Events have been now replaced by the more performance friendly Mutation Observer.
The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature which was part of the DOM3 Events specification.
Refer: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
(function() {
var ev = new $.Event('display'),
orig = $.fn.css;
$.fn.css = function() {
orig.apply(this, arguments);
$(this).trigger(ev);
}
})();
$('#element').bind('display', function(e) {
alert("display has changed to :" + $(this).attr('style') );
});
$('#element').css("display", "none")// i change the style in this line !!
$('#element').css("display", "block")// i change the style in this line !!
http://fiddle.jshell.net/prollygeek/gM8J2/3/
changes will be alerted.
Tried this on firefox, works http://jsfiddle.net/Tm26Q/1/
$(function(){
/** Just to mimic a blinking box on the page**/
setInterval(function(){$("div#box").hide();},2001);
setInterval(function(){$("div#box").show();},1000);
/**/
});
$("div#box").on("DOMAttrModified",
function(){if($(this).is(":visible"))console.log("visible");});
UPDATE
Currently the Mutation Events (like DOMAttrModified used in the
solution) are replaced by MutationObserver, You can use that to
detect DOM node changes like in the above case.
I just Improved ProllyGeek`s answer
Someone may find it useful.
you can access displayChanged(event, state) event when .show(), .hide() or .toggle() is called on element
(function() {
var eventDisplay = new $.Event('displayChanged'),
origShow = $.fn.show,
origHide = $.fn.hide;
//
$.fn.show = function() {
origShow.apply(this, arguments);
$(this).trigger(eventDisplay,['show']);
};
//
$.fn.hide = function() {
origHide.apply(this, arguments);
$(this).trigger(eventDisplay,['hide']);
};
//
})();
$('#header').on('displayChanged', function(e,state) {
console.log(state);
});
$('#header').toggle(); // .show() .hide() supported
A catch-all jQuery custom event based on an extension of it's core methods like it was proposed by different people in this thread:
(function() {
var ev = new $.Event('event.css.jquery'),
css = $.fn.css,
show = $.fn.show,
hide = $.fn.hide;
// extends css()
$.fn.css = function() {
css.apply(this, arguments);
$(this).trigger(ev);
};
// extends show()
$.fn.show = function() {
show.apply(this, arguments);
$(this).trigger(ev);
};
// extends hide()
$.fn.hide = function() {
hide.apply(this, arguments);
$(this).trigger(ev);
};
})();
An external library then, uses sth like $('selector').css('property', value).
As we don't want to alter the library's code but we DO want to extend it's behavior we do sth like:
$('#element').on('event.css.jquery', function(e) {
// ...more code here...
});
Example: user clicks on a panel that is built by a library. The library shows/hides elements based on user interaction. We want to add a sensor that shows that sth has been hidden/shown because of that interaction and should be called after the library's function.
Another example: jsfiddle.
I like plugin https://github.com/hazzik/livequery It works without timers!
Simple usage
$('.some:visible').livequery( function(){ ... } );
But you need to fix a mistake. Replace line
$jQlq.registerPlugin('append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html', 'prop', 'removeProp');
to
$jQlq.registerPlugin('show', 'append', 'prepend', 'after', 'before', 'wrap', 'attr', 'removeAttr', 'addClass', 'removeClass', 'toggleClass', 'empty', 'remove', 'html', 'prop', 'removeProp');

Pattern for widgets in unobstructive javascript

I'm using JS and jQuery for the first time after a lot of experience with Java and C++. I'm loving jQuery's idea of $(document).on('click', 'btn-selector', react), but for more complex widgets I'm finding myself in the same rut over and over: in each react handler, I have to look up the widget as a whole and reconstruct all my knowledge about it.
For example, I'm making a simple widget out of <input>s with which the user can make a grading scale: 90 maps to an A, 80 maps to a B, etc. When one of the inputs changes, I want to check to make sure that the inputs are still in order (your scale can't go 90, 70, 80, for example).
So, I have something like
Actual
$(document).on('click', '.scale-input', function() {
var widget = $(this).closest('.scale-widget-container');
ensureLevelsAreInOrder(widget);
});
Almost every single handler has to have this first line to find its context. I'd much rather have code that looks like this:
Preferred
$(document).on('click', '.scale-input', ensureLevelsAreInOrder);
The problem is that in this form, ensureLevelsAreInOrder only has a reference to the input that changed, not the larger context.
In Java or C++, I would have called a constructor on the widget, and each input would have a handler with the context baked in via member variables. I could do something similar with
$(function() {
$('.scale-widget-container').scaleWidget();
});
with scaleWidget() setting up the contextualized handlers, but the page I'm working with loads a lot of its html with ajax and I don't have a reliable time to run that initialization.
Is this a common problem that we just have to deal with if we don't want JS in our HTML, or is there a solution I haven't come across yet?
Not sure what it is you're after exactly, but you don't seem to touch on two quite important concepts when it comes to JS: the event object, and closures. Both of these are open to you to get what you need:
event object:
The callback function is passed an argument, that describes the event itself, and references the elements affected by that event, This isn't exclusive to jQ (just google addEventListener), but it's quite handy:
$(document).on('click', '.scale-input', function(e)//<-- e is our event
{
console.log(e);//check console
});
Which, in vanilla JS would look like this:
document.addEventListener('click', function(e)
{
if (!e.className.test(/\bscale\-input\b/))
{
return e;
}
console.log(e);
}, false);
Another thing you might want to consider is enclosing references to whatever it is you need in an IIFE's scope:
(function()
{
var containers = $('.scale-widget-container'),
localBool = false,
asMany = 'varsAs you need',
previousScales = [],
inputs = $('.scale-input');//references to all DOM nodes you mention
$(document).on('click','.scale-input',function(e)
{
console.log($(this));
console.log(containers);
previousScales.push(this.value);//or something
console.log(previousScales);
//and so on.
});
}());
Hope this helped
Update:
If IE isn't a browser you don't care about that much, you could use one of the DOM-modified events, specifically DOMTreeModified:
(function()
{
var nodes = [];//<-- store current nodes here, if applicable
nodes.containsNode = function(node)
{
var i;
for (i=0;i<this.length;i++)
{
if (this[i] && this[i] === node)
{//node is set, return its index
return i;
}
}
//node not found, return -1
return -1;
};
document.body.addEventListener('DOMSubtreeModified',function(e)
{
var all = document.getElementsByClassName('scale-input'),
i;
for (i=0;i<all.length;i++)
{
if (nodes.containsNode(all[i]) === -1)
{
nodes.push(all[i]);//add new
}
}
},false);
}());
More on the mutation events, and their issues, on the DOM events wiki

Backbone - Anyway to check if an event has been bound before?

I'm doing this inside one of my Views:
render: function($options) {
...
this.collection.on('reset', _(function() {
this.render($options);
}).bind(this));
....
}
The problem is, whenever reset as well as the re-rendering has been triggered, a new reset binding will be created, resulting 2x, 4x, 8x, etc. times of re-rendering as it goes on.
It's a bit tricky to move the binding into the initialize section (which should solve this issue), however since it's not an option, is there any other solution available, like having Backbone checking if this event has been bound before, or something?
Moving your binding to initialize would be best but assuming that you have good reasons not to, you could just set a flag:
initialize: function() {
var _this = this;
this._finish_initializing = _.once(function($options) {
_this.collection.on('reset', function() {
_this.render($options);
});
});
//...
},
render: function($options) {
this._finish_initializing($options);
//...
}
There are lots of different ways to implement the flag, _.once just nicely hides the flag checking. You could also trigger an event in render have a listener that unbinds itself:
initialize: function() {
var finish_initializing = function($options) {
/* your binding goes here ... */
this.off('render', finish_initializing);
};
this.on('render', finish_initializing, this);
},
render: function($options) {
this.trigger('render', $options);
//...
}
That's the same logic really, just dressed up in different clothes. You could also use an explicit flag and an if in render or assign a function to this._finish in initialize and that function would delete this._finish.
like having Backbone checking if this event has been bound before, or something?
Sure..
!!this.collection._events["render"]
Backbone doesn't expose most of the API required to make it useful. That's alright, use it anyway.
First, define your event handler function as a named function
var self = this;
var onReset = function() {
self.render($options);
}
Then, defensively unbind the function each time render is called
this.collection.off('reset', onReset);
this.collection.on('reset', onReset);
I recently accomplished this using a javascript variable.
Outside of any functions, I declared:
var boundalready =0
Then, inside the function:
if (boundalready == 0){
boundalready = 1;
bind(this);
};
This worked for me pretty well.

BackboneJS: Trigger form validation on form submit from view

So, I've started using Backbone.js to structure my javascript code and have modular applications, and I came across a problem reggarding events.
I want to make a simple View that handles forms and validates them. In the future I would like to add all the javascript functionallity like live validation, hover effects, etc.
This is the simplified code I have right now:
var Form = Backbone.View.extend({
attributes: {
att1 = 'att1',
att2 = 'att2'
},
events: {
'submit': 'validateFields'
},
initialize: function(element) {
this.el = $(element);
},
validateFields: function() {
alert(this.attributes.att1); //do something
return false;
}
});
var f = new Form('#formid');
The problem I had is that the validateFields function is not called when I submit the form. I also tried using this on the constructor:
this.el.bind('submit', this.validateFields);
Now, that last code works but the "this" inside the validation function would be the $('#formid') object, instead of my Form object.
Backbone uses jQuery's delegate method to bind the events to the callbacks in the events hash.
Unfortunately the delegate method does not work for the submit event in IE See comment in Backbone source
An easy fix is to add this line of code to your render method.
render: function() {
//render the view
$(this.el).submit(this.validateFields);
}
You will also need to bind the context for validateFields in the initialize method
initialize: function() {
_.bindAll(this, 'render', 'validateFields');
}
Try setting your el in other way:
var f = new Form({el: '#formid'});
In this case you can even remove initialize method (or change it):
var Form = Backbone.View.extend({
// ...
initialize: function() {
// Some code
},
// ...
});
As far as this code is concerned: this.el.bind('submit', this.validateFields);. If you want to preserve Form object context you should use binding:
this.el.bind('submit', _.bind(this.validateFields, this)); // using Underscore method
this.el.bind('submit', $.proxy(this.validateFields, this)); // using jQuery method

Attach multiple jQuery plugins onto a single element

According to the jQuery plugin development guides from the Internet, the common practice of developing a jQuery plugin would be:
(function($) {
$.fn.myplugin = function(options){
//...
//Plugin common characteristic
//e.g. default settings
//...
//Attach to each desired DOM element
return this.each(function(){
//Instantiation stuff...
});
}
})(jQuery);
$(document).ready(function(){
$(".someclass").myplugin();
})
It seems to me that, if the elements with class "someclass" have been attached to another plugin, once those elements are going to attach to "myplugin", they will lose the original relationship to the previously attached plugin. I'm not sure if my thinking is completely correct. Please advise if any mis-understood.
Thank you!
William Choi
An element isn't "attached" to a plug-in. A plug-in just adds further methods to the jQuery wrapper for a matched set of elements. So just as the jQuery wrapper has parent and find, it also has the plug-in's myplugin method. These can all coexist as long as there are no naming conflicts.
It's true that if two different plug-ins both try to change something about the elements that cannot be two things at once (a plug-in that changes the foreground color to "blue" and another changing the foreground color to "red"), then they'd collide if you called both of the two plug-ins methods on the same element set. But that's just like two calls to css.
In particular, remember that there can be multiple event handlers assigned to the same event on the same element, so plug-ins that hook events need not necessarily conflict with one another (unless one of them stops the event during handling).
Here's an example of two plug-ins that act on the matched set of elements, but in non-conflicting ways:
plugin1.js:
(function($) {
$.fn.foo = function() {
this.css("background-color", "#b00");
return this;
};
})(jQuery);
plugin2.js:
(function($) {
$.fn.bar = function() {
this.css("color", "white");
return this;
};
})(jQuery);
Usage:
$("#target").foo();
$("#target").bar();
or even
$("#target").foo().bar();
Live example
Now, if both the foo and bar plug-ins tried to set the foreground color, the one called later would win.
Here's an example of a pair of plug-ins that both want to handle the click event, but do so in a cooperative way:
plugin1.js:
(function($) {
$.fn.foo = function() {
this.click(function() {
$("<p>Click received by foo</p>").appendTo(document.body);
});
return this;
};
})(jQuery);
plugin2.js:
(function($) {
$.fn.bar = function() {
this.click(function() {
$("<p>Click received by bar</p>").appendTo(document.body);
});
return this;
};
})(jQuery);
Usage:
jQuery(function($) {
$("#target").foo().bar();
});
Live example
There's no magical relationship going on. There's no central registry or snap-ins that "belong" to any one element or to any one plug-in.
Javascript objects are just hacked-up functions; when you "attach a plugin" to an element, you're just calling some third-party library function that does something to that element, and possibly stores some internal data to assist with its animation throughout the session.
So there is nothing legally stopping you from "attaching" multiple plug-ins to the same element, though of course whether they'll be logically compatible is quite another question.

Categories