Friday, April 26, 2013

Generating sprites using Sass and Compass



Here it goes another post about new client-side stuff. Though, depends what you consider being 'new', sometimes I catch myself discovering new technology or tool, and thinking 'could it be that I was doing all of this manually rather than just letting code do the work for me?', and feel like dumbass. Can't really tell that same thing happened with Sass and Compass, since principally I'm not front-end developer, neither did I ever created sprites before. But faced with challenge of creating one, I remembered someone mentioned generating sprites via Compass, so was thinking giving it a try.

And there it was, magic in having all classes and background positions generated for you, rather than doing it by hand. Impressive!

Now, little background. What is Sass? In short I would describe it as stylesheet language that compiles to CSS, but has many advantages like mixins, nested rules (that make your stylesheets more readable), and many more, for reference see sass-lang.com. Comapred to less that serves to same purpose as Sass, but written in Javascript (can be executed either server-side via nodejs, or client-side in browser), Sass is written in Ruby. So, if you're thinking going Sass / Compass way first step would be to download and install Ruby. If you're running OSX or Linux, I think you'll find your own way of doing this (via apt or yum, or brew and ports for OSX), and Windows users can find ruby installers here. Once you have ruby set up and running, installing Sass is easy typing following into your command prompt:











Next on the list is Compass, CSS authoring tool (stated by Compass official page), from what I could tell, it's quite usefull for making your own grid system, creating sprites, has a collection of cross-browser compatible CSS3 mixins, and so on... Anyways, installing compass is not much different than installing Sass:

gem install compass













Once we have Sass and Compass installed, next thing is to create Compass project:

compass create project












Now, you have to create directory where your images directory, and subdirectories for each sprite you wish to create (I've used arrow in this example). Once images grouped by directories, you need to tell compass where to search for your images files by modifying config.rb file in project root:

compass sprite image listing















Config.rb file contents:


http_path = "/"
css_dir = "stylesheets"
sass_dir = "sass"
images_dir = "img"
javascripts_dir = "javascripts"

Now all left to do is to tell compass to create our sprite. Let's create file names arrows-sprite.scss and instruct Compass / Sass to create our sprite, and compile file into pure CSS:


/** arrows-sprite.scss file content **/

@import "arrows/*.png";
@include all-arrows-sprites;"

compile cass to css








And here's where all the magic happened, we got our CSS file with classes matching image filenames, and png sprite. See contents of generated CSS, and sprite image below:



I guess you'll want to remove all of those ugly-looking comments that refer to Cass line of code that generated output. You do this by adding following line to config.rb :

line_comments = false

And you'll get clean-looking CSS as here.
You can checkout demo page on js bin below

If you wish to learn more about the ways you can make advantage of sprite-generating functions of sprite, check out official page.

Tuesday, April 16, 2013

Going native JS: Select elements without jQuery - document.querySelector


Almost 7 years after jQuery's first stable release, most of the client-side development population got used to querying DOM through  jQuery (or any JavaScript framework equivalent like ExtJS) selectors. But since times are changing for web devs, how about going native? After brief research, I've came up with W3C's selector API. In short, you can select DOM elements with two methods recommended by referenced W3C papers, and implemented by most of modern browsers:

document.querySelector(selectString)

and

document.querySelectorAll(selecString)

As expected, first method will return zero or one items, while latter method will return all items selected. In terms of selectString syntax, all valid CSS selectors supported by browser, can be passed to this functions. As per browser compatibility, all modern browsers are supporting selectors API, according to caniuse, only troublemaker is IE7, that is slowly dying. 

And for the last simple demo :
(and code)

        document.querySelector('#demoButton')
           .addEventListener('click',function(){
               alert('This page has ' +
                      document.querySelectorAll('div').length + 
                      ' div elements');
       });

Wednesday, April 10, 2013

TDD - jQuery simple slideshow plugin and unit testing

As a primarily back-end developer, first time I heard someone mentioning unit testing of front-end components, it did not ring a bell how useful it could be, I was naively thinking that front-end testing should end on tester clicking on screen. Of course, I was terribly wrong.

Faced with challenge of delivering front-end heavy project, I had to dig deeper into modern tools used throughout development of client-side apps. I'll try to cover in this blog posts simple workflow in TDD approach when writing jQuery plugin.

For an example let's build simple plugin that displays image gallery with fade effect. Technical and functional requirements would be:
  • Ability to initialize plugin with list of image paths
  • Specify time between images being changed
  • Ability to hook onto event when image is being changed
  • Ability to jump to specific image by it's index
  • Control whatever images are looping or not
  • Specify transition and animation duration
  • Specify container size
As unit testing library I'll use qunit. All of the source below can be found on github.

Test 1: Initialization

Upon initialization, we want to make sure that first image is displayed. At this point we'll assume that images are being displayed via css background property, rather than img tag.

       
   var arrImageList =['http://www.westminsterkennelclub.org/breedinformation/working/images/samoyed.jpg',
                      'http://static.tumblr.com/o9yg6la/zsymaeh1o/samoyedaa.jpg',
                      'http://fotos.tsncs.com/img/20120410/7273071/cachorros-de-samoyedo-24703108_3.jpg',
                      'http://t2.gstatic.com/images?q=tbn:ANd9GcRRV6TveS6Vc6qkRJZKE- vvdFwKQjGCqxbQz82_VTev5HAGy6K8'],
       $module;

   test("moduleInit",function(){  
        var expectedCssProp = "url(" + arrImageList[0] + ")";  
       
        $module = $('#module').imageSlideshow({  
            imageList : arrImageList  
        });  
   
        equal($module.find('.current-img').css('background-image'),
              expectedCssProp,
              "First background image upon init OK");  
   });  

Test 2: Make sure that image is changed after specified time amount

We want to specify time between image transitions in milliseconds due initialization via transitionTime plugin configuration parameter, while animationTime parameter while control how long image transitions last. So we'll have to test on every (animationTime + transitionTime * k + overhead) milliseconds if images has changed and is matching next image in array. Overhead mentioned here is to make sure any overhead operations complete, and I'll set it to 20ms which is more than enough on modern computers to execute. K parameter represent ordinal number of last transition happened. We'll have to use asynchronous calls for this, and thus utilize qunit's start() and stop() functions (more details about these functions on qunit official site)


      var arrImageList =['http://www.westminsterkennelclub.org/breedinformation/working/images/samoyed.jpg',
                'http://static.tumblr.com/o9yg6la/zsymaeh1o/samoyedaa.jpg',
                'http://fotos.tsncs.com/img/20120410/7273071/cachorros-de-samoyedo-24703108_3.jpg',
                'http://www.dogbreedinfo.com/images17/SamoyedHolly3YearsOld.JPG'],
            $module;
    
    
            test("initAndTransition",function(){
      
                var expectedCssProp = "url(" + arrImageList[0] + ")",           
                    transitionTestCounter =1,
                    animationTime = 300,
                    transitionTime = 2000;
      
                $module = $('#module').imageSlideshow({
                    imageList : arrImageList,
                    animationTime : animationTime,
                    transitionTime : transitionTime
                });
                
                //test if first image displayed OK
                equal($module.find('.current-img').css('background-image'),
                      expectedCssProp,"First background image upon init OK");
               
                stop();
               
                    var testTransition = function(){
                    //notify qunit we're to starting test so test context can be initalized
                    start();
                   
                    expectedCssProp = "url(" + arrImageList[transitionTestCounter++] + ")";            
                    
                    equal($module.find('.current-img').css('background-image'),
                           expectedCssProp,(transitionTestCounter-1) + " transition OK");
                    
                    if(transitionTestCounter < arrImageList.length){
                        //notify qunit test is finished
                        stop();
                        
                        //on each successive execution after first wait for transitionTime + overhead (20ms) to test again
                        setTimeout(testTransition, transitionTime + 20);
                    }
                };
                //schedule first async call
                setTimeout(testTransition,animationTime + transitionTime + 20);            
            });

Note that wit this code we're not only testing IF image changed, we are also testing WHEN it changed. We could predict that plugin fires event that can notify outside world about event happening, and use event handler to test for image url, but this way we have two flies in one hit.

Test 3: Control whatever animation loops or not

We'll just extend test above to include loop plugin configuration variable that controls whatever animation loops once last image has been reached. Below is modified test code that includes described test

    test("initAndConfiguration",function(){
      
                var expectedCssProp = "url(" + arrImageList[0] + ")",transitionTestCounter =1
                ,animationTime = 300
                ,transitionTime = 2000;
      
                $module = $('#module').imageSlideshow({
                    imageList : arrImageList,
                    animationTime : animationTime,
                    transitionTime : transitionTime,
                    loop: false
                });
     
                equal($module.find('.current-img').css('background-image'),
                         expectedCssProp,"First background image upon init OK");

                stop();
                var testTransition = function(){
                    //notify qunit we're to starting test so test context can be initalized
                    start();
                    expectedCssProp = "url(" + arrImageList[transitionTestCounter++] + ")";            
                    equal($module.find('.current-img').css('background-image'),
                               expectedCssProp,(transitionTestCounter-1) + " transition OK");
                    
                    if(transitionTestCounter < arrImageList.length){
                        //notify qunit test is finished
                        stop();
                        //on each successive execution after first wait for
                        // transitionTime + overhead (20ms) to test again

                        setTimeout(testTransition, transitionTime + 20);
                    } else {
                         stop();
                        setTimeout(function(){
                            start();
                            
                            //after last transition and loop set to false
                            // we're expected to see last image in array as image shown
                            expectedCssProp = "url(" + arrImageList[arrImageList.length - 1] + ")";     
                            equal($module.find('.current-img').css('background-image'),
                                        expectedCssProp,"Loop false OK");                 
                                                    
                        },transitionTime + 20);
                    }
                };
                setTimeout(testTransition,animationTime + transitionTime + 20);               
            });

Note that in test above we're only covering case when loop is set to false. To have more code coverage, we would have to write opposite test that sets loop configuration variable to true, and asserts whatever image swithced to first after reaching end of array.

Test 4: Verify that plugin events and methods for changing image work properly

WIll try to hook onto plugin's imageChanged event,  and ability that plugin user can jump to any image in given gallery. Also, we'll introduce another configuration variable  autoplay that controls whatever plugin automatically starts changing images upon initialization - we're not testing effect of this variable on plugin at this point, but it's assumed if we're about to control slideshow programatically, there ain't going to be any auto-advance. Notice usage of ok() function form qunit which validates boolean value.


            test('moveAndEvents',function(){
                var eventFired = false,
                
                //variable eventFired shall be asserted later in code
                eventHandler = function(){
                    eventFired = true;
                },
                expectedCssProp = "url(" + arrImageList[2] + ")"
            
                $module = $('#module').imageSlideshow({
                    imageList : arrImageList,
                    animationTime : 300,
                    transitionTime : 2000,
                    loop: false,
                    autoPlay: false
                });
     
                $module.bind('imageChanged',eventHandler);
                
                //display third image
                $module.imageSlideshow('displayImage',2);
                
                //test if third image shown
                equal($module.find('.current-img').css('background-image'),
                                      expectedCssProp,"displayImgae method OK");
                
                ok(eventFired, "imageChanged event fired OK");               
            });

Event with lots of code above there is still space for improvement and even greater code coverage with tests. One of ideas is testing for loop:true, testing whatever object fired with imageChanged event carries proper information and image currently displayed, validating css configuration object (not mentioned above, examine source code for more details). Whole source code for both plugin and tests can be found at github. Below you can see demo of plugin in action, showing pictures of beautiful sammy dogs: