At work I recently needed to write a jQuery plugin that uses Bootstrap styling, so
I looked at how
alert.js is
implemented, since it is a fairly small plugin. In this post I'll go over
alert.js
line by line, and do my best to explain what is going on. You should
have a decent understanding of JavaScript and some familiarity with jQuery, but
I'll assume no knowledge of how to write a jQuery plugin or anything specific to
Bootstrap.
Overview
Let's start with an overview of how the Alert plugin can be used. You can find the Bootstrap Alert plugin documentation here. Essentially there are two ways to use Alerts, and this applies to other Bootstrap plugins as well
-
Use the jQuery plugin function directly. For Alerts this looks like this
$(".alert").alert() // To close the alert: $(".alert").alert("close")
-
Data API: use data-* attributes to configure in a more declarative style. For Alerts, the primary data attribute is
data-dismiss
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
We'll see how the code in alert.js
supports both of these styles.
What's this +function
business?
Okay, let's start at the top. The first line of code is this
+function ($) {
We have an unnamed (anonymous) function, and a +
in front of it. What's going
on there? For the moment let's ignore the body of this JavaScript function
definition and look at how this function gets invoked:
+function ($) { // ... }(jQuery);
This is an example of the Immediately Invoked Function
Expression
(IIFE) pattern. The intent of an IIFE is to introduce stronger lexical scoping
in JavaScript and to allow for some functions defined within the IIFE to remain
private. There are several variations of this pattern. One way to do it is to
have the IIFE return an object with references to public functions while leaving
some functions private. In this case, nothing is returned. Instead, the plugin
is added to the jQuery
object. Because nothing is returned by the IIFE, we
can use a trick to get the JavaScript interpreter to call our anonymous function
immediately, and that trick is to put an unary operator in front of the function
definition.
The sole argument passed to the function is jQuery
for the $
parameter. Why
passing jQuery
as the $
parameter, why not just use $
from the global
scope like so?
+function () { $('#foo').hide(); // etc... }();
We could do that, but that would only work as long as $
refers to the jQuery
function. There are other libraries that define the $
reference, so in order
to interoperate with them we'll assume only that there is a jQuery
reference
in the global scope. But it's nice that we can essentially alias jQuery
to
$
within our plugin definition in a lexically scoped and tidy way.
use strict
The next line of alert.js
is a String, 'use strict'
:
+function ($) { 'use strict';
According to the Strict mode article on the Mozilla Developer Network
ECMAScript 5's strict mode is a way to opt in to a restricted variant of JavaScript. Strict mode isn't just a subset: it intentionally has different semantics from normal code.
Perl has something similar. 'use strict'
is a way of running your JavaScript
code through the interpreter in a less loosy-goosy manner. Here are some
benefits of running in strict mode:
- Impossible to accidentally create global variables (for example, mistyped variable names)
- Severals classes of silent errors become thrown exceptions
- Simplifies variable naming which allows JavaScript interpreters to better
optimize code (for example,
eval
doesn't introduce new variables into the surrounding scope) - Several security enhancing restrictions are added
See the linked article for more information.
Alert class definition
Now for the Alert class definition. Our Alert class has a constructor and one
method, close
. First, the dismiss
variable is defined. This is a jQuery
selector that is used here in the constructor to bind to the click event from a
DOM element that matches this selector. The way this works is that any DOM
element that is el
or a descendant of el
that has a data-dismiss
attribute
with value alert
that fires a click event will be handled by this.close
:
var dismiss = '[data-dismiss="alert"]' var Alert = function (el) { $(el).on('click', dismiss, this.close) }
Alert close
function
The close
method is defined for the Alert
class by adding it to Alert
's
prototype. This method does the following:
- Figure out the
$parent
element (i.e., the alert div) that should be closed. There are a few different ways to determine the$parent
and we'll look at those shortly. - Trigger a custom event before closing the
$parent
and one after it is closed. - Use a CSS3 transition to fade the
$parent
and then remove it.
Finding the $parent
to close
First, figure out the $parent
element. Here's the first part of close
:
Alert.prototype.close = function (e) { var $this = $(this) var selector = $this.attr('data-target') if (!selector) { selector = $this.attr('href') selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7 } var $parent = $(selector) if (e) e.preventDefault() if (!$parent.length) { $parent = $this.hasClass('alert') ? $this : $this.parent() } // ...
In the first line of close
, this
refers to the element that is the target of
click event. So in the standard case where you have a close <button>
inside a
<div class="alert">
element, this
is the <button>
. However, close
could
be called through the plugin function, as we'll see later, so this
might refer
to the alert itself (<div class="alert">
). For now, we'll consider the case
that this
refers to the close button.
The first option to check is a data-target
attribute on the close button. The
value is expected to be a selector. You would use it like this:
<div id="myalert"> <button class="close" aria-hidden="true" data-target="#myalert" data-dismiss="alert" type="button">×</button> <strong>Holy guacamole!</strong> Best check yo self, you're not looking too good. </div>
Click on the button should cause <div id="myalert">
to be closed.
The second option to check is the href
attribute. This can be used likewise.
This line then checks to see if anything matches those options:
var $parent = $(selector)
If we haven't set data-target
or href
on our close button, or if we have but
they don't match anything, then $parent
will have a length of 0. That's what
is checked for next. If we still haven't found $parent
there are a couple
more things to try.
So the third option is to check to see if $this
has class alert. If it does
then we assume that this is the element that should be faded and removed.
The fourth option, the default if none of the other checks matched anything, is
to set $parent
to the parent of $this
.
Like I said earlier, it's possible for the close
method to be called directly
in which case $this
is the alert itself. In that case, it is the third
option that would typically be used for figuring out the $parent
.
Custom events for before and after closing
Before actually closing the alert, close
dispatches a close.bs.alert
event
on the $parent
element.
$parent.trigger(e = $.Event('close.bs.alert')) if (e.isDefaultPrevented()) return
The first line creates the custom event, using jQuery's $.Event
and also sets
it to the variable e
. The name of the event, "close.bs.alert", utilizes
jQuery event namespaces. This just makes it
possible to listen for the close event just from Bootstrap's Alert plugin by
binding to the close.bs.alert
event. It also makes it easier in Bootstrap to
remove event handlers. For example, all of the following would work to remove an
event handler bound to close.bs.alert
:
$(el).off('close.bs.alert'); $(el).off('close.alert'); $(el).off('close.bs'); $(el).off('.bs.alert'); $(el).off('.alert'); $(el).off('.bs');
For removing, only one of the namespaces need match. I put together a JSFiddle to demonstrate jQuery event namespaces that I think helps demonstrate how they work:
$(document).on('close.bs.alert', function() { console.log("close.bs.alert handler"); }); $(document).on('close.alert', function() { console.log("close.alert handler"); }); $(document).on('close.bs', function() { console.log("close.bs handler"); }); $(document).on('close', function() { console.log("close handler"); }); console.log("triggering close..."); $(document).trigger('close'); // triggers 'close.bs.alert', 'close.alert', // 'close.bs' and 'close' console.log("triggering close.bs..."); $(document).trigger('close.bs'); // triggers 'close.bs.alert' and 'close.bs' console.log("triggering close.alert..."); $(document).trigger('close.alert'); // triggers 'close.bs.alert' and // 'close.alert' console.log("triggering close.bs.alert..."); $(document).trigger('close.bs.alert'); // triggers 'close.bs.alert' console.log("Remove close.alert handlers and triggering close..."); $(document).off('close.alert'); // removes close.bs.alert and close.alert //$(document).off('.alert'); // removes close.bs.alert and close.alert as well $(document).trigger('close'); // triggers 'close.bs' and 'close'
So that's event namespacing and triggering the close event. The next line
is to return if the close event had preventDefault
called on it. The idea
is that a listener for the close event could call e.preventDefault()
to
prevent the alert from actually closing.
The other custom event that the Alert plugin dispatches is the closed event
which is named closed.bs.alert
. It is dispatched when the Alert is actually
removed from the DOM. We'll see the code to trigger this event in the next
section where we'll look at how the Alert is faded out and removed from the DOM.
Removing and fading out the Alert
Now we'll look at how the Alert is faded out and removed from the DOM.
$parent.removeClass('in') function removeElement() { $parent.trigger('closed.bs.alert').remove() } $.support.transition && $parent.hasClass('fade') ? $parent .one($.support.transition.end, removeElement) .emulateTransitionEnd(150) : removeElement()
Fading out the Alert is accomplished by using a CSS3 transition. To understand how it works, let's look at the CSS:
.fade { opacity: 0; transition: opacity 0.15s linear 0s; } .fade.in { opacity: 1; }
When an element has the fade and in classes, its opacity is 1 and when
it only has the fade class its opacity is 0. The transition
CSS property
defines how quickly and in what way the opacity
property changes from 0 to 1
when in is added to an element that already has fade. In this case
however, the close
method is removing the in class which causes the
opacity to transition, linearly, from 1 to 0, causing it to fade out of view and
become invisible.
Making the Alert invisible is not enough however. We want to actually remove it from the DOM. If we don't then it will still take up space on the web page, it just won't be visible. To accomplish this we need to know when the transition finishes. Once the transition finishes we can then remove the Alert from the DOM.
The removeElement
function is defined so as to trigger the closed.bs.alert
and then remove the Alert from the DOM. In an ideal world, we'd just bind the
transitionend event to the removeElement
handler. However there are a few
complications:
- It's possible that the Alert doesn't have the fade class, in other words,
it is not desired that it actually fade out. That's fine, the transition is
optional. That's what is checked for with
$parent.hasClass('fade')
. - It's possible that this browser doesn't support CSS transitions. That's what
$.support.transition
is checking for. More on this later. - It's possible that even if the browser supports CSS transitions that it
doesn't call the transition end event transitionend. The
$.support.transition.end
property holds the name of the transition end event for that browser. - Finally, it's possible that the transition end event won't be
called. So we need to
programmatically dispatch the event if it isn't called. That's what
emulateTransitionEnd(150)
is doing.
You can see the tests Bootstrap does to determine the transition end event
name in
transition.js.
Also defined in transition.js
is the emulateTransitionEnd
function.
So let's go line by line. First, we'll remove the in class which will start
the transition from opacity 1 to 0 over the course of 0.15 seconds. Next,
define a function, removeElement
, that will be called when the transition
finishes. We'll bind removeElement
to the transition end event.
removeElement
will trigger the closed.bs.alert
event, the second custom
event, and it will remove the Alert from the DOM.
So next we have a ternary expression. If the browser supports transitions and
the alert (the $parent
) has class fade, then bind removeElement
to the
transition end event exactly once ($parent.one(...)
). The call to
emulateTransitionEnd(150)
will set a 150 millisecond timeout and dispatch the
transition end event if it hasn't already been dispatched (again, see
transition.js
for details). If the browser doesn't support Transitions or $parent
doesn't
have the fade class, then just call removeElement
.
Alert jQuery Plugin Definition
Ok, a brief recap. We've defined the Alert class and it's single close
method,
which does almost all of the heavy lifting for this plugin. Now it's time to
actually define the Alert class as a jQuery plugin. Here's the code:
var old = $.fn.alert $.fn.alert = function (option) { return this.each(function () { var $this = $(this) var data = $this.data('bs.alert') if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }) } $.fn.alert.Constructor = Alert
First thing here is to capture the current value of $.fn.alert
into the old
variable. This is used later in noConflict
, and it just a way to prevent this
alert
plugin from interfering with another alert
plugin with the same name.
The $.fn.alert
function is defined next. The way this function will be used is
like so
$('.alert').alert();
So this
refers to the result of a jQuery selector, which means it is an Array
of DOM elements. So for each matching DOM element we'll run this function:
function() { var $this = $(this) var data = $this.data('bs.alert') if (!data) $this.data('bs.alert', (data = new Alert(this))) if (typeof option == 'string') data[option].call($this) }
Within the scope of this function, this
refers to the current DOM element.
$this
is a jQuery wrapped reference to the DOM element.
When this function creates an Alert
instance for this DOM element it will
store it as a reference associated with the DOM element. var data =
$this.data('bs.alert')
is checking if we've already created an Alert
instance
for this DOM element. The third line of the function creates the Alert
instance (data = new Alert(this)
) if it hasn't already been created and it
will be saved to the bs.alert key:
if (!data) $this.data('bs.alert', (data = new Alert(this)))
Finally, you may have noticed that the plugin function accepts an options
argument. This is a common jQuery plugin pattern. There aren't any actual
options for the Alert
class, but options
can also be a string with the name
of a method that you want to invoke on the Alert
instance. That's what is
being done here. data
, remember, is a reference to the Alert
instance.
data[option]
therefore refers to a function on the Alert
instance. It is
called with $this
as the context (that is, within the method invoked by
call
, this
will refer to $this
).
Also, as a jQuery plugin best practice, the result of calling this.each
is
returned to enable chaining of jQuery function calls.
The other thing we haven't talked about yet is this line:
$.fn.alert.Constructor = Alert
See this StackOverflow answer for a long
explanation. but, basically, if
we don't do this then the Alert
constructor remains private to the closure
in which we defined it. By creating the Constructor
reference, other code can
directly instantiate an Alert
instance without needing to invoke it indirectly
through the jQuery API.
Alert noConflict
Now we're at the noConflict
function:
$.fn.alert.noConflict = function () { $.fn.alert = old return this }
This is a simple convention for handling the situation that there is another
jQuery plugin also called alert
. This function sets $.fn.alert
to its old
value and returns this
which refers to the $.fn.alert
function defined
above.
Alert Data API
Finally we get to the Data API. This allows for purely declarative use of the Alert plugin, no JavaScript calls necessary.
$(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
This binds the 'click' event for any DOM element in the page matching the
dismiss
selector to Alert
's close
method. Recall that the dismiss
selector was defined as [data-dismiss="alert"]
. So any clickable element in
the page that has the attribute data-dismiss
with value alert will have
its click event handled by Alert
's close
method.
The difference with the Data API is that there is no actual Alert
instance
created. Instead Alert.prototype.close
method is used directly, as a pure
function. The way close
will be invoked in this case is basically identical to
how it will be invoked via the binding we see in the Alert
constructor:
$(el).on('click', dismiss, this.close)
Additional Resources
- Using CSS Transitions - at Mozilla Developer Network
- transitionend
3 comments:
This is a great breakdown Marcus. Exactly what I was looking for while I try to add custom functionality to a site following the same format and syntax of the existing Bootstrap plugins. Thanks!
Nice work, thx!
Super useful! I found myself looking at Alert for the same reasons, and this answered the last lingering questions before I finished up my plugin. I wish I'd seen this before I'd started =)
Post a Comment