HTML5 components: Ad-hoc components, Part 2

Supporting event listeners, animating elements with CSS3, and injecting content into the DOM

In this series, HTML5 maven David Geary shows you how to implement HTML5 components. In this installment, finish implementing the sophisticated ad-hoc slider component introduced in "Ad-hoc components, Part 1." Learn how to incorporate event listeners, animate the slider's knob, and inject the slider into an existing Document Object Model (DOM) tree.

Share:

David Geary, Author and speaker, Clarity Training, Inc.

David GearyThe author of Core HTML5 Canvas, David Geary is also the co-founder of the HTML5 Denver User's Group and the author of eight Java books, including the best-selling books on Swing and JavaServer Faces. David is a frequent speaker at conferences, including JavaOne, Devoxx, Strange Loop, NDC, and OSCON, and is a three-time JavaOne Rock Star. He is the author of the HTML5 2D game development, JSF 2 fu, and GWT fu article series for developerWorks.



07 May 2013

Also available in Chinese Russian Japanese

HTML5 is a sophisticated programming platform for the web, with powerful capabilities such as the canvas element, CSS3, and an event model. HTML5 provides all the underlying functionality that you need to implement your own components.

About this series

A standard UI component model for HTML5 is still evolving. In this series, HTML5 expert David Geary shows how to create your own ad-hoc HTML5 components with existing technology — and how to leverage specifications that are on the way to defining a bona fide HTML5 component system.

This article continues the discussion of the slider component began in this series' first installment. You'll see how to incorporate event listeners, animate the slider's knob, and inject the slider itself into an existing DOM tree. More specifically, you will learn how to:

  • Use alternate controls with the slider
  • Implement support for registering and firing change listeners
  • Animate HTML elements with CSS3 transitions
  • Inject content into the DOM tree
  • Customize ad-hoc components with element attributes

Alternate slider controls

Figure 1 shows a variant of the simple application from Part 1 that uses the slider component. This variant uses links, instead of buttons, to increment and decrement the slider's value. For those controls, you can use any element that generates mouse-click events.

Figure 1. Using links, instead of buttons
Screenshot of the slider with links, instead of buttons

The user can also change the slider's value by clicking in its rail or dragging its knob. The section "Dragging the knob" in Part 1 explains how the slider facilitates dragging of its knob. In this article's "Animating the slider's knob" section, you'll see how the slider adjusts the knob's position with a CSS3 transition when the user clicks in the slider's rail.

Listing 1 shows the HTML for the application illustrated in Figure 1.

Listing 1. HTML for the slider application with links, instead of buttons
<html>
   <head>
      ...
   <head>

   <body>
      <div id='slider-component'>
         <div id='controls'>
            <a href='#' class='slider-control' id='decrement'>-</a>
            <a href='#' class='slider-control' id='increment'>+</a>

            <div id='slider'>  <!-- slider goes here -->  </div>
         </div>

         <div id='readout'>0</div>
      </div>
   </body>

   <script type="text/javascript" src="lib/slider.js"></script>
   <script type="text/javascript" src="sliderExample.js"></script>
</html>

The HTML in Listing 1 is straightforward. Two links and the slider reside in a controls DIV, which in turn resides in a slider-component DIV. When a user clicks on either link, the application's JavaScript handles the click event, as shown in Listing 2.

Listing 2. Link-click events
var slider = new COREHTML5.Slider('black', 'cornflowerblue', 0),
...

document.getElementById('decrement').onclick = function (e) {
   slider.knobPercent -= 0.1;
   slider.redraw(); 
   updateReadout();
}

document.getElementById('increment').onclick = function (e) {
   slider.knobPercent += 0.1; 
   slider.redraw(); 
   updateReadout();
}

The event handlers in Listing 2 increment and decrement the slider's value (stored in the slider's knobPercent attribute), redraw the slider, and update the readout element.

When the slider's value changes, the application updates the readout element with a change listener that's attached to the slider, as shown in Listing 3.

Listing 3. Slider change events
var readoutElement = document.getElementById('readout');

function updateReadout() {
   if (readoutElement)
      readoutElement.innerHTML = slider.knobPercent.toFixed(2);
}

slider.addChangeListener(updateReadout);

Notice that the readout element is optional; if it doesn't exist, the updateReadout() method does nothing.

The next section shows you how the slider implements support for change listeners.

The application shown in Figure 1 appends the slider to the DOM with the slider's appendTo() method:

slider.appendTo('slider'); // Append the slider to the DOM element with an ID of slider
slider.draw();

This article discusses the implementation of that method in the "Appending the slider to a DOM element" section.


Supporting slider change events

In the preceding section you saw how the application in Figure 1 keeps an optional readout in sync by attaching a change listener to the slider. Listing 4 shows how the slider lets developers add change listeners and how it fires change events to those listeners.

Listing 4. Adding change listeners and firing events
COREHTML5.Slider = function {
   ...
   this.changeEventListeners = [];
   ...
};

COREHTML5.Slider.prototype = {
   ...

   addChangeListener: function (listenerFunction) {
      this.changeEventListeners.push(listenerFunction);
   },

   fireChangeEvent: function(e) {
      for (var i=0; i < this.changeEventListeners.length; ++i) {
         this.changeEventListeners[i](e);
      }
   },
   ...
};

Every slider contains an array of functions. When the slider's value changes, the fireChangeEvent() method iterates over that array, invoking each function in turn. Listing 5 shows how the slider invokes its fireChangeEvent() method.

Listing 5. Firing change events
COREHTML5.Slider.prototype = {
   addMouseListeners: function () {
      this.knobCanvas.addEventListener('mousemove', function(e) {
         var mouse = null,
             percent = null;

         e.preventDefault();

         if (slider.draggingKnob) {
            slider.deactivateKnobAnimation();
            
            mouse = slider.windowToCanvas(e.clientX, e.clientY);
            percent = slider.knobPositionToPercent(mouse.x);

            if (percent >= 0 && percent <= 1.0) {
               slider.fireChangeEvent(e);
               slider.erase();
               slider.draw(percent);
            }
         }
      }, false);

   },

As the user drags the slider's knob, a mouse-move event listener attached to the knob canvas adjusts the slider's value, fires a change event, and erases and redraws the slider. That event listener also deactivates the slider's knob animation while the user is dragging the knob to ensure that the animation doesn't interfere with the user's dragging of the knob.

Now that you've seen the circumstances under which the slider deactivates knob animation, let's take a look at how that animation works.


Animating the slider's knob

Figure 2 shows how the slider animates its knob. The top screenshot in Figure 2 was taken just before the user clicked in the slider's rail. The bottom two screenshots show the subsequent movement of the knob to the cursor's location. (Figure 2 is a poor approximation of the actual animation; to get the full effect, download the code and try it.)

Figure 2. Animating the slider
Series of three screenshots that demonstrate the slider animation

As I mentioned in the section "Creating and initializing the slider" in Part 1, the slider draws its rail and knob in separate canvas elements. It would be simpler if the slider drew the rail and knob in the same canvas; however, CSS3 transitions work only on individual elements, so the knob must be in a separate canvas.

In the preceding section you saw that the slider invokes its deactivateKnobAnimation() method to deactivate the knob animation while the user is dragging it. That method, along with its inverse —activateKnobAnimation()— is shown in Listing 6.

Listing 6. Activating and deactivating the knob animation
COREHTML5.Slider.prototype = {
   ...

   activateKnobAnimation: function () {
      var transitionString = "margin-left " +
          (this.knobAnimationDuration / 1000).toFixed(1) + "s";

      this.knobCanvas.style.webkitTransition = transitionString;
      this.knobCanvas.style.MozTransition = transitionString;
      this.knobCanvas.style.OTransition = transitionString;
      this.knobCanvas.style.transition = transitionString;
   },

   deactivateKnobAnimation: function () {
      slider.knobCanvas.style.webkitTransition = "margin-left 0s";
      slider.knobCanvas.style.MozTransition = "margin-left 0s";
      slider.knobCanvas.style.OTransition = "margin-left 0s";
      slider.knobCanvas.style.transition = "margin-left 0s";
   },
   ...
};

Naming differences among browsers

Like many HTML5 features, CSS3 transitions were first implemented as browser-specific functionality and then standardized by the W3C. To ensure that transitions work for all browser versions that support them, code such as Listing 6 must accommodate all the browser-specific variants of CSS attribute names.

The activateKnobAnimation() method shown in Listing 6 programmatically adds a CSS3 transition to the knob canvas element's margin-left CSS attribute, accounting for browser variations on the name of the transition attribute itself. As a result of that transition, the browser smoothly animates the knob canvas from one position to another when the margin-left attribute changes. The animation's duration is specified in milliseconds with the slider's knobAnimationDuration attribute.

The deactivateKnobAnimation() method changes the duration of the CSS3 transition to zero seconds, effectively disabling the animation.

When the user clicks in the slider's rail, the event handler shown in Listing 7 moves the slider's knob to the click location.

Listing 7. Clicking in the slider's rail
COREHTML5.Slider.prototype = {
   ...
   addMouseListeners: function () {
      ...
   
      this.railCanvas.onmousedown = function(e) {
         var mouse = slider.windowToCanvas(e.clientX, e.clientY),
             startPercent,
             endPercent;

         e.preventDefault();

         startPercent = slider.knobPercent;
         endPercent = slider.knobPositionToPercent(mouse.x);

         slider.animatingKnob = true;

         slider.moveKnob(mouse.x);
         slider.trackKnobAnimation(startPercent, endPercent);
      };
   },
   ...
};

The slider's moveKnob() method, shown in Listing 8, moves the knob by setting the knob canvas element's margin-left CSS attribute. Setting that attribute triggers the CSS3 knob animation, provided that knob animation is active.

Listing 8. Moving the slider's knob
COREHTML5.Slider.prototype = {
   ...

   moveKnob: function (position) {
      this.knobCanvas.style.marginLeft = position - this.knobCanvas.width/2 + "px";
   },
   ...
};

CSS attribute names in JavaScript

Many CSS attribute names contain hyphens, which can't be used in JavaScript attribute names. That unfortunate impedance mismatch requires you to convert CSS attribute names containing hyphens to camel-case JavaScript attribute names; for example, margin-left becomes marginLeft and padding-top becomes paddingTop.

Besides moving the slider's knob, the rail canvas's mouse-down event handler invokes the slider's trackKnobAnimation() method, as shown in Listing 7. That method, which is discussed in the next section, keeps the slider's value in sync with the knob throughout the CSS3 transition's corresponding animation.


Tracking CSS3 transitions

You can detect the end of a CSS3 transition with an event listener. The slider component does just that by invoking the slider's addKnobTransitionListener() method from the slider's constructor, as Listing 9 illustrates.

Listing 9. Adding knob transition listeners
COREHTML5.Slider = function(strokeStyle, fillStyle, knobPercent, knobAnimationDuration) {
   ...
   this.createDOMTree();
   this.addMouseListeners();
   this.addKnobTransitionListener();
};

The slider's addKnobTransitionListener() method, shown in Listing 10, adds a transition listener to the knob canvas, once again accounting for naming differences among browsers.

Listing 10. CSS transition listeners
COREHTML5.Slider.prototype = {
   ...

   addKnobTransitionListener: function () {
      var BROWSER_PREFIXES = [ 'webkit', 'o' ];

      for (var i=0; i < BROWSER_PREFIXES.length; ++i) {
         this.knobCanvas.addEventListener(
            BROWSER_PREFIXES[0] + "TransitionEnd", // Everything but Mozilla

            function (e) {
               slider.animatingKnob = false;
            }
         );
      }

      this.knobCanvas.addEventListener("transitionend", // Mozilla
         function (e) {
            slider.animatingKnob = false;
         }
      );
   },      
   ...

The browser invokes the transition listener in Listing 10 when the knob transition's animation is complete. That listener simply sets the slider's animatingKnob attribute to false, which causes the slider to discontinue its tracking of the animation knob. That tracking is implemented by the slider's trackKnobAnimation() method, shown in Listing 11.

Listing 11. Tracking the knob animation
trackKnobAnimation: function (startPercent, endPercent) {
      var count = 0,
          KNOB_ANIMATION_FRAME_RATE = 60,  // fps
          iterations = slider.knobAnimationDuration/1000 * KNOB_ANIMATION_FRAME_RATE + 1,
          interval;

      interval = setInterval( function (e) {
         if (slider.animatingKnob) {
            slider.knobPercent = startPercent +
                                 ((endPercent - startPercent) / iterations * count++);

            slider.knobPercent = slider.knobPercent > 1.0 ? 1.0 : slider.knobPercent;
            slider.knobPercent = slider.knobPercent < 0 ? 0 : slider.knobPercent;

            slider.fireChangeEvent(e);
         }
         else { // Done animating knob 
            clearInterval(interval);
            count = 0;
         }
      }, slider.knobAnimationDuration / iterations);
   },
   ...   
};

Although you can detect the end of a CSS3 transition's animation with an event listener, as illustrated in Listing 10, you cannot detect the intermediate steps of that animation, which is necessary for the slider to fire change events as the knob animates.

CSS3 transitions versus animations

To animate the slider's knob, I used CSS3 transitions, which are relatively simple to use but do not support event notification during the transition's corresponding animation. I could've used CSS3 animations, which are more difficult to use but do support event notifications during the animation.

Because it's not possible to detect the steps of a CSS3 transition's animation, the slider's trackKnobAnimation() method uses setInterval() to approximate the steps of the animation and fire change events at each step.

Now that you've seen how to animate the slider's knob with CSS3 transitions, let's see how to append the slider to an existing DOM element.


Appending the slider to a DOM element

The section "Drawing the slider" in Part 1 discussed the implementation of the slider component without reference to CSS3 transitions. That implementation results in a DOM tree for the slider, as shown in Figure 3.

Figure 3. The slider's DOM tree
Illustration of the slider's DOM tree, consisting of two canvas elements and a DIV

The slider's constructor creates a DIV and two canvas elements — one each for the slider's rail and its knob — and appends the canvases to the DIV. When the application shown in Listing 1 subsequently appends the slider to an existing DOM element, the slider appends the slider's enclosing DOM element to the existing element, as illustrated in Figure 4.

Figure 4. Merging the slider's DOM tree
Diagram illustrating the merging of the slider's DOM tree

The slider's constructor calls the slider's createDOMTree() method, as shown in Listing 12.

Listing 12. Creating the slider's DOM tree
COREHTML5.Slider = function(strokeStyle, fillStyle, knobPercent, knobAnimationDuration) {
   ...
   this.createDOMTree();
   this.addMouseListeners();
   this.addKnobTransitionListener();
};

Listing 13 shows the slider's createDOMTree() method.

Listing 13. The createDOMTree() method
COREHTML5.Slider.prototype = {
   ...

   createDOMTree: function () {
      var self = this;

      this.domElement = document.createElement('div');

      this.domElement.appendChild(this.knobCanvas);
      this.domElement.appendChild(this.railCanvas);
   },
   ...
};

By the time the slider invokes createDOMTree(), it has already created the canvases for the knob and the rail. The createDOMTree() method creates the slider's enclosing DIV element and adds the existing knob and rail canvases to that element.

After the slider has created its DOM tree, the stage is set to append the slider to existing DOM elements with the slider's appendTo() method, as shown in Listing 14.

Listing 14. The appendTo() method: Attaching the slider to an existing DOM element
COREHTML5.Slider.prototype = {
   ...

   appendTo: function (elementName) {
      if (typeof element === 'string') {
         document.getElementById(element).
            appendChild(this.domElement);
      }
      else {
         element.appendChild(this.domElement);
      }

      this.resize();
   },
   ...
};

Setting both CSS size and canvas element size

The last four lines of the slider's setKnobCanvasSize() method in Listing 15 illustrate an important point: You should not set CSS width and height attributes for a canvas element without also setting the width and height attributes of the canvas element to the same values. That's because the CSS attributes apply only to the canvas element, whereas the canvas width and height attributes apply to both the canvas element and its drawing surface. If you just set the CSS attributes, a mismatch will occur between the canvas element size and the size of its drawing surface, causing the browser to scale the canvas's drawing surface to fit the element. As Listing 15 shows, the slider's resize() method sets the sizes of the slider's rail and knob canvases, in addition to the size of the slider itself.

You can pass the appendTo() method either a string representing an element's ID or the element itself; either way, the method appends the slider's enclosing DOM element to the specified element and resizes both the canvas and the slider's enclosing DOM element. That resizing takes place in the resize() method and the methods it invokes, all of which are shown in Listing 15.

Listing 15. Resizing the slider to fit its enclosing element
COREHTML5.Slider.prototype = {
   ...

   setRailCanvasSize: function () {
      var domElementParent = this.domElement.parentNode;

      this.railCanvas.width = domElementParent.offsetWidth;
      this.railCanvas.height = domElementParent.offsetHeight;
   },

   
   setKnobCanvasSize: function () {
      this.knobRadius = this.railCanvas.height/2 -
                        this.railContext.lineWidth;

      this.knobCanvas.style.width = this.knobRadius * 2 + "px";
      this.knobCanvas.style.height = this.knobRadius * 2 + "px";
      this.knobCanvas.width = this.knobRadius*2;
      this.knobCanvas.height = this.knobRadius*2;
   },

   setSliderSize: function() {
      this.cornerRadius = (this.railCanvas.height/2 -
                           2*this.VERTICAL_MARGIN)/2;

      this.top = this.HORIZONTAL_MARGIN;
      this.left = this.VERTICAL_MARGIN;

      this.right = this.left +
                   this.railCanvas.width - 2*this.HORIZONTAL_MARGIN;

      this.bottom = this.top + 
                   this.railCanvas.height - 2*this.VERTICAL_MARGIN;
   },

   resize: function() {
      this.setRailCanvasSize();
      this.setKnobCanvasSize();
      this.setSliderSize();
   },
   ...
};

Customizing components

The slider component, as it currently exists, is quite useful. But no matter how much utility a component provides, odds are good that people will want to customize it in some manner. Those customizations can be as simple as color changes, as illustrated in Figure 5, or as substantial as supporting vertical sliders (which the slider discussed in this article doesn't do).

Figure 5. An aqua slider
Screenshot of the slider in a new color: aqua

Listing 16 shows the addition of two attributes to the slider-component DIV element for the slider's stroke and fill colors.

Listing 16. Specifying attribute elements
<!DOCTYPE html>
<html>
   <head>
      ...
   </head>
   
   <body>
      <div id='title'>A custom slider</div>

      <p>
         <div id='slider-component' stroke='blue' fill='aqua'>
            ...
         </div>
      </p>
   </body>

   <script type="text/javascript" src="lib/slider.js"></script>
   <script type="text/javascript" src="sliderExample.js"></script>
</html>

The application's JavaScript uses the getAttribute() method, as shown in Listing 17, to get the values for the stroke and fill attributes.

Listing 17. Accessing element attributes
var sliderElement = document.getElementById('slider-component'),
    slider = new COREHTML5.Slider(sliderElement.getAttribute('stroke'),
                                  sliderElement.getAttribute('fill'),
                                  0),
    ...

Subsequently, it uses those values to create the slider.


Next time

The next article in this series will discuss the W3C's "Introduction to Web Components" specification and show you how to implement the slider component with Shadow DOM, custom elements, and templates. See you next time.


Download

DescriptionNameSize
Sample codewa-html5-components-2-code.zip6KB

Resources

Learn

Get products and technologies

Discuss

  • Get involved in the developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Web development on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Web development
ArticleID=928792
ArticleTitle=HTML5 components: Ad-hoc components, Part 2
publish-date=05072013