AngularJS Directive for Mobile Sliders
11 Apr 2013
For a five-part introduction to AngularJS, check out ng-okevin’s Angular.
I ditched jQuery Mobile’s sliders for my growing AngularJS poker
app. It had served as the app’s poker
bet slider. All I wanted was a simple slider widget from jQuery Mobile, but the
UI framework wanted me to marry it in return. It had its way with my HTML
markup, littering my DOM elements with selfish classes and attributes, like
an open garbage truck on the freeway. My CSS rules felt oppressed like liberal
college students as jQuery Mobile’s CSS files applied forceful rules my
top-level html
and body
elements. The kicker? The slider quickly
became deprecated, not working on newer versions of Firefox. Never even saw it
work on Chrome.
AngularJS Directives
My disappointment digresses. The point is: I assembled my own customizable mobile slider that hooked into AngularJS in the form of a directive. Let us begin with the HTML, which AngularJS enhances with declarative two-way bindings.
<div slider class="slider" min="0" max="9001" step="10">
<span></span>
</div>
Unfortunately, this is not the most exciting display of AngularJS’s abilities.
Though by giving the element my slider
attribute, I make the element a
template for our AngularJS directive named slider
, which is where all the
magic will happen. There are other ways to syntactically bind the directive to
the element through the restrict
parameter, we even make it look like a
<slider>
tag. Note that the following Javascript code snippets are parts
of a whole.
var myApp = angular.module('my-app',['my-app.directives']);
var directives = angular.module('my-app.directives', []);
directives.directive('slider', function() {
return {
link: function(scope, element, attrs) {
// Linking function.
// {{ initialize }}
// {{ event handler }}
// {{ scope updater }}
// {{ scope watcher }}
}
};
});
Directives teach HTML new tricks, allowing us to write reusable components and
widgets. The link function allows us to register event handlers and watches on
the scope, all binded to our template. link
supplies the AngularJS scope,
the element acting as the directive template, and attributes of the element.
##Initialize
Let us get some initialization out of the way.
// Linking function.
var $element = $(element);
var $bar = $('span', $element);
var step = attrs.step;
var width;
var offset;
var mouseDown = false;
element.on('mousedown touchstart', function(evt) {
mouseDown = true;
if (!width) {
width = $element.width();
} if (!offset) {
offset = $bar.offset().left;
}
});
element.on('mouseup touchend', function(evt) {
mouseDown = false;
});
This just allows us to determine whether the user is currently dragging the slider with a click or touch.
##Event Handler
Whenever the user drags the slider, we want to update its binded scope value. Vice-versa, whenever the binded scope value changes, we want to visually update the slider’s fill.
// Throttle function to 1 call per 25ms for performance.
element.on('mousemove touchmove', _.throttle(function(evt) {
if (!mouseDown) {
// Don't drag the slider on mousemove hover, only on click-n-drag.
return;
}
// Calculate distance of the cursor/finger from beginning of slider
var diff;
if (evt.pageX) {
diff = evt.pageX - offset;
} else {
diff = evt.originalEvent.touches[0].pageX - offset;
}
Above is the math driving the slider. We want to calculate how far the slider is dragged relative to the max value of the slider, in terms of a percentage.
##Scope Updater
We then take that percentage of the max value to attain the slider’s value. That value is then applied to the scope.
// Allow dragging past the limits of the slider, but impose min/max values.
if (diff < 0) {
scope.sliderValue = attrs.min;
$bar.width('0%');
} else if (diff > width) {
scope.sliderValue = attrs.max;
$bar.width('100%');
// Set the value to percentage of slider filled against a max value.
} else {
var percent = diff / width;
$bar.width(percent * 100 + '%');
scope.sliderValue = Math.round(percent * attrs.max / step) * step);
}
// Let all the watchers know we have updated the slider value.
scope.$apply();
}, 25));
We have enough to calculate the slider’s value. We set the width of the inner span to create the visual effect of the slider, and update the scope’s slider value.
##Scope Watcher
This accomplishes one-way data binding; if we change the slider, we change the slider’s value in the scope. But we also need to account for if the slider’s value in the scope is changed somewhere else, we need to visually update the slider. We accomplish this by registering a watcher on the scope.
scope.$watch('sliderValue', function(sliderValue) {
$bar.width(sliderValue / attrs.max * 100 + '%');
});
When the slider’s value in the scope is updated somewhere else, the watcher will trigger our callback function. We change the slider’s fill percentage to respectively match the new slider value.
##CSS
Setting the width of the inner span changes the fill of the slider.
.slider {
@include box-shadow(inset 0 -1px 1px rgba(255,255,255,0.3));
background: rgb(10, 10, 10)
border-radius: 5px;
height: 20px;
padding: 10px;
position: relative;
> span {
@include gradient(rgb(43,194,83) 37%, rgb(84,240,84) 69%);
@include box-shadow(
inset 0 2px 9px rgba(255,255,255,0.3)\,
inset 0 -2px 6px rgba(0,0,0,0.4)
);
background-color: rgb(43,194,83);
border-radius: 20px 8px 8px 20px;
font-weight: bold;
display: block;
height: 100%;
overflow: hidden;
position: relative;
}
}
To view the code, inspect the code for this page as I inlined it in the HTML.
Up Next
By now you might have started on your own sweet AngularJS app. But testing is an important part of development. How to test such an opinionated framework? Check out my post on Mock Unit Testing AngularJS Services with Karma and Jasmine.