Creating a JavaScriptMVC Slider

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 javascriptmvc, 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[](https://github.com/jupiterjs/mxui/blob/master/slider/test/funcunit/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.

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