Creating a JavaScriptMVC Slider

by Justin Meyer

Creating a JavaScriptMVC Slider

Justin Meyer JavaScriptMVC makes it easy to write and organize copious amounts of JavaScript code. And although this usually means working among its Model-View-Controller layers to create something amazing, often, you'll find yourself wanting to make a lightweight, reusable UI widget. This demo walks you through creating a basic slider widget.

posted in Development on December 12, 2010 by Justin Meyer

JavaScriptMVC makes it easy to write and organize copious amounts of
JavaScript code. And although this usually means working among its
Model-View-Controller layers to create something amazing, often, you’ll
find yourself wanting to make a lightweight, reusable UI widget. This
demo walks you through creating a basic slider widget.

Demo (What We’re Making)

Mxui
slider

(the
code
)

Step 0: Following along

To follow along with this guide, download a fresh copy of
JavaScriptMVC
and
unzip it (or update your current copy).

Step 1: Creating a widget play-place.

For the vast majority of applications we build, we have two root
folders:

  • The application folder

    Has all the files and tests specific to the application: models,
    business logic controllers, and views, used to customize the output
    of reusable widgets.

  • The widget folder

    Has all the potentially reusable widgets. These are things that can
    be used outside a specific application. For Bitovi, we’ve been
    putting all of these in mxui, the jQueryMX UI
    library
    .

The first step of creating a widget is creating the folder and files our
widget needs. Fortunately, the jquery/generate/plugin generator does
this. By typing the following in the command line, it creates a page,
empty source file, and tests for our widget.

js jquery/generate/controller mxui/slider

Step 2: Creating the API and test page.

Before you can write tests for a UI widget, it helps to know what you’re
going to test. So, the first step is to create a test page and design
the widget’s API.

Designing an API is a subtle and tricky business. Try to keep things as
simple as possible by only building the ‘essence’ of a widget. For me,
it’s just a draggable element whose value changes based on its position
relative to its container. The slider will read the container’s
properties and set itself up accordingly.

This is in contrast to something like
jQueryUI’s or
YUI’s
slider, where the slider control includes the containing element and
mandates other conventions (like specific classNames).

By focusing on just the basics, our slider will be lightweight and more
flexible.

So, to our test page, I’ll add a minimal HTML structure for the slider,
some CSS, and an element to show the value of the slider:

The HTML:

<div id='container'>
  <div id='slider'></div>
</div>
<input id='value'/>

The CSS:

#container {
  width: 330px;
  border: solid 1px black;
}
#slider {
  width: 26px;
  border: solid 2px green;
  background-color: #008000;
  height: 30px;
}

Finally, I want to setup my slider and listen for changes on it. So I
add the following in a script tag:

$("#slider").mxui_slider({
  min: 1,  // the minimum value
  max: 10, // the maximum value
  val: 5   // the starting value
}).change(function(ev, value){
  // show the value
  $('#value').val(value)
});

You can see the result of these pages in
slider.html.
Now we have something we can test!

Step 3: Creating a test.

The generator produces a test script in
mxui/nav/slider/slider_test.js.
I’ll add the following tests:

test("dragging changes value",function(){
  S("#slider").drag("+30 +0", function(){
    equals( S("#value").val(), 6);
  }).drag("-60 +0",function(){
    equals( S("#value").val(), 4);
  });
})

test("dragging out of bounds", function(){
  S("#slider").drag("+400 +0", function(){
    equals( S("#value").val(), 10);
  }).drag("-400 +0", function(){
    equals( S("#value").val(), 1);
  })
})

FuncUnit’s
syntax is so awesome, I’m not going to explain what these do. If I were
making this slider for more than a demo, I’d also add quite a few more
tests. But this is a good start.

If we run these tests, it fails miserably. So, lets move to building the
slider!

Step 4: Creating the slider plugin:

I’ll build the plugin in steps that paralleled how I actually built the
widget.

Setup the slider widget

The first thing I’ll do is open
slider.js
and steal the two plugins I’m certain are needed:

steal.plugins('jquery/controller',
              'jquery/event/drag')
     .then(function($){

Then I define my slider widget:

$.Controller("Mxui.Slider",{})

Configuring the drag behavior

I want my element to be draggable, so I’ll make a draginit function:

$.Controller("Mxui.Slider",{
  "draginit" : function(el, ev, drag){}
});

At this point, I can start dragging my element around the page. But, I
want to keep the element in its container. JavaScriptMVC has a drag
limit plugin that does exactly this. So I’ll add
'jquery/event/drag/limit' to the list of steal.plugins and make
draginit look like:

"draginit" : function(el, ev, drag){
  drag.limit(this.element.parent())
}

At this point, my drag can’t escape it’s parent. With the following 5
lines, we’ve got something that looks very much like a slider:

$.Controller("Mxui.Slider",{
  "draginit" : function(el, ev, drag){
    drag.limit(this.element.parent())
  }
});

I also want my slider to snap in increments. The
jquery/event/drag/step plugin limits a drag’s position to every X
pixels relative to some container. This is perfect! The only problem is
that I need to know that pixel value. To help, I’ll create a
getDimensions function that gets and caches this and other values:

getDimensions : function(){
  var spots = this.options.max - this.options.min,
      parent = this.element.parent();

  //total movable area
  this.widthToMove = parent.width() - 
                     this.element.outerWidth();

  //space between spots
  this.widthOfSpot = this.widthToMove / 
                     this.options.spots;
}

The following diagram might help these calculations make sense:

$.Controller creates a jQuery plugin. In this case, it creates
$().mxui_slider(). The first argument passed to the mxui_slider
becomes this.options on the controller. I used the min and max values,
the width of the container element, and the outer width of the slider to
calculate the width of one spot.

Now in draginit, I call getDimensions and use the spot width to set the
step value:

  "draginit" : function(el, ev, drag){
    this.getDimensions();
    drag.limit(this.element.parent())
        .step(this.widthOfSpot, this.element.parent());
  }

Calculating and sharing the value

I want my slider to behave similar to a form element. When it’s value
changes (a drag motion is complete), it will trigger a change event with
the value of the slider.

To do this, I need to know where the draggable element is in relation to
the inner left side of the container. I’ll calculate this in
getDimensions with JavaScriptMVC’s
curStyles
plugin:

  var styles = parent.curStyles("borderLeftWidth",
                                "paddingLeft"),
      leftSpace = parseInt( styles.borderLeftWidth ) + 
                  parseInt( styles.paddingLeft )|| 0;
  this.leftStart = parent.offset().left + spaceLeft;

Now I can listen for dragend, which fires when the user finishes
dragging, calculate the slider’s value (which spot its in), and trigger
a change event with the value:

  "dragend" : function(el, ev, drag){
    var left =  this.element.offset().left - this.leftStart;
    var spot = Math.round( left / this.widthOfSpot );
    this.element.trigger("change", spot+this.options.min)
  }

I also want to be able to get the value of the slider programmatically
like:

$('#slider').mxui_slider('val') -> '5'

And be able to set the value (and update the UI) like:

$('#slider').mxui_slider('val',7);

To do this, I’ll add a val function to my controller like:

  val : function(value){
    this.getDimensions();
    if(value){
      //move slider into place
      this.element.offset({
        left: this.leftStart+
                Math.round( (value-this.options.min)
                  *this.widthOfSpot )
      })
      this.element.trigger("change", value)
    }else{
      var left = this.element.offset().left - 
                 this.spaceLeft;
      return Math.round( this.leftStart/this.widthOfSpot)+
             this.options.min;
    }
  }

Model <–> View binding

As a quick side note, this plugin can now work with the ‘jquery/tie’
plugin so you can have bi-directional binding between models and
sliders. For example:

$.Model('Person')
person  = new Person({age : 7})
$('#slider').mxui_slider({min: 0, max: 10}).tie(person, "age")

This allows us to change the value of person and it will automatically
update the slider and vice versa. See an example
here.

Accept an initial value

We want to be able to initialize our controller with a value. We’ll add
an init method that sets an inital value if provided:

  init : function(){
    this.element.css({
    position: 'relative'
    })
    if(this.options.val){
      this.val(this.options.val)
    }
  }

Default options

Finally, we want to make our plugin as easy to initalize as possible so
we give it default option values like:

$.Controller("Mxui.Slider",{
  defaults : {
    min: 0,
    max: 10
  }
},
{ ... });

If users don’t provide their own options, these are used.

Step 5: Sharing your slider

If you update steal, or have a very recent download, you can run:

js steal/pluginifyjs mxui/slider -nojquery

To create a mxui.slider.js script that can be used without JavaScriptMVC
and only requires jQuery.

Conclusion

This example is meant to touch on a few key concepts:

  • Test driven development of traditionally difficult to test UI
    behavior.
  • High-level JavaScriptMVC organization.
  • Widget design.
  • Using drag events and controller.
  • Creating standalone widgets.

But most importantly, it might make you less afraid to write your own
widgets. There’s room for improvement. For example, it should only
trigger a change event if the value changes. However, in only 53 lines,
we’ve made a halfway decent slider that covers 95% of what most apps
need.

blog comments powered by Disqus

Free Weekly Online JavaScript Training!

Justin Meyerposted in Development, Training on April 27, 2015 by Justin MeyerBitovi is hosting a weekly 45 minute JavaScript training for intermediate to advanced JavaScript developers. Join us to learn all about JavaScript, jQuery, application architecture, and of course, CanJS, StealJS, and other technology.

Using CanJS 2.2 with StealJS and NPM

David Lueckeposted in Development, Open Source on April 23, 2015 by David LueckeCanJS can be used in many different module formats like AMD and CommonJS and package managers like Bower and NPM. In this post we will talk about using CanJS with the new StealJS and NPM.

Set Algebra with can-set

Justin Meyerposted in Development, Open Source on April 22, 2015 by Justin Meyercan-set is a client and server side JavaScript utility that makes set algebra easy. This article talks about the design goals of can-set and introduces a few cases where you might find it useful.

Contact Us
(312) 620-0386 | contact@bitovi.com
 or cancel