HTML5 components: Ad-hoc components, Part 1

Gain an understanding of the HTML5 component model and begin implementing ad-hoc components

This article launches a short series by David Geary on implementing HTML5 components. In this installment, get an overview of HTML5 component technology and start learning how to implement a sophisticated slider component from scratch.

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.



19 February 2013

Also available in Chinese Russian Japanese Vietnamese

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.

Component models are an important part of any programming platform because they provide a standard set of UI elements and infrastructure for building more.

HTML5 is a relatively young technology with a set of even younger specifications that aim to standardize the implementation of HTML5 components and their integration with other components. The HTML5 component model is currently defined by three specifications:

  • "Introduction to Web Components"
  • "Shadow DOM"
  • "HTML Templates"

The functionality in these three specifications involves encapsulating component DOM trees (Shadow DOM), creating custom tags for components that page authors can use in their HTML pages (custom HTML elements), and declaratively specifying Shadow DOMs and insertion points (HTML templates). (See the "HTML5 component specifications" sidebar for more detail.)

HTML5 component specifications

"Introduction to Web Components" is a high-level overview of Web Components and is primarily intended to spawn other specifications, such as the "Shadow DOM" and "HTML Templates" specifications.

The "Shadow DOM" specification is a World Wide Web Consortium (W3C) Working Draft, which effectively means it can be considered part of HTML5. Shadow DOM gives ordinary developers extraordinary powers to encapsulate the components they implement, much the same way that browser vendors have long been able to encapsulate low-level elements in their video elements and range inputs.

The "HTML Templates" specification is not yet a Working Draft, but it has reportedly gained considerable mindshare among developers and has a good chance of becoming one. Templates let you declaratively specify Shadow DOMs that have insertion points, into which you can insert elements selected with CSS selectors.

See Resources for links to each specification.

But to implement components in the first place, you need a completely different skill set that includes the ability to create appealing graphics, layer HTML elements to support interesting interactions, and support event listeners. This series covers all these aspects of HTML5 component development:

  • Ad-hoc components
  • Shadow DOM, templates, and custom elements
  • Mozilla's X-Tag

In this article and the next in the series, see how to implement ad-hoc HTML5 components from scratch, without any support from the new specifications. Later installments will explore the specifications and show you how to incorporate Shadow DOM, custom elements, and templates into your HTML5 component-building repertoire.

More specifically, in this article, learn how to:

  • Implement ad-hoc HTML5 components
  • Draw appealing graphics with the canvas element
  • Layer canvas elements to implement special effects
  • Implement real-time dragging of an HTML element

Building one HTML5 component out of several

Figure 1 shows an image-processing component that is built with other, more-general components: icons, a toolbar, sliders, and an image-display area.

Figure 1. An image-processing component
Combination of two screenshots of an image-viewer component: The top screenshot illustrates use of the resize slider; the bottom screenshot shows the image after it has been rotated with the rotate slider.

Three of the icons in the toolbar — resize, rotate, and brightness — are associated with sliders. When you click on one of those icons, the image-processing component displays a slider and a couple of related buttons underneath the toolbar. The top screenshot in Figure 1 illustrates the use of the resize slider; the bottom screenshot shows the image after it has been rotated with the rotate slider.


The slider component

Sliders consist of a rail and a knob; you change the slider's value by moving the knob along the rail. The position of the knob determines the slider's value (or vice versa if you programmatically set the value and redraw the slider). That value — a number between 0.0 and 1.0 — represents the percentage of the rail that lies to the left of the knob; for example, when three-quarters of the rail is to the left of the knob, the slider's value is 0.75.

Figure 2 shows a simple application that illustrates how to use a slider. Besides the slider, the application contains two buttons that incrementally move the slider's knob to the right (+) or left (-). The application also provides a readout underneath the slider that displays the slider's current value. See Resources for a link to run the application; you can also download the source code.

Figure 2. A slider
Screenshot of a slider

The user can move the slider's knob in three ways:

  • Click one of the buttons to increment (+) or decrement (-) the slider's value by 10 percent.
  • Drag the knob.
  • Click in the rail, outside the knob, to move the center of the knob to the click location.

When the slider's value is set programmatically — which is the case when you activate the plus (+) or minus (-) buttons or enable clicking in the slider's rail outside the knob — the knob's movement is animated by a CSS transition with an ease-out function that slows the knob as it approaches its final destination. The slider component sets the transition's properties in JavaScript.

To use a slider, you instantiate one and append it to an existing DOM element, as shown in Listing 1.

Listing 1. Creating a slider
var slider = new COREHTML5.Slider( // All of the following arguments are optional
   'navy',           // Stroke color
   'cornflowerblue', // Fill color
   0.5,              // Initial slider value
   500);             // Knob animation duration in milliseconds
...

slider.appendTo('someDiv');  // Appends the slider to a DOM element with the ID someDiv

The slider's appendTo() method resizes the slider so that it expands or shrinks to fit the element that the slider is attached to.

Why not use a range input?

There are two reasons to implement a slider component even though HTML already provides a range input. First, you might want to ensure the same look and feel among all browsers, which is the case for the image-processing component shown in Figure 1. Second, some browsers, such as Firefox, insist on mere text input to represent a range. In many cases, that is insufficient.

The slider's features are:

  • Attaches to DOM elements with an appendTo() method
  • Automatically resizes to fill the DOM element to which the slider is attached
  • Registers change event listeners with an addChangeListener() method
  • Fires change events to change listeners when the slider's knob moves
  • Animates the knob with a CSS transition when the user clicks in the rail

Sliders are instances of COREHTML5.Slider to avoid namespace collisions. It's not too far-fetched to imagine someone implementing a slider with a more obvious name, such as Slider, which would replace any existing global objects with an identical name. It's unlikely, however, that anyone would come up with COREHTML5.Slider on their own.

The COREHTML5.Slider constructor's arguments are all optional; all the values have reasonable defaults. Table 1 lists key COREHTML5.Slider methods.

Table 1. Key slider methods
MethodDescription
appendTo(elementId)Appends the slider's DOM element to the element whose ID matches the value passed to the method.
addChangeListener(listenerFunction)Adds a change listener function to the slider. When the slider's knob changes position, the slider fires an event to all of its change listeners.
draw()Draws the slider's rail and knob.
erase()Erases the slider.
redraw()Erases and then draws the slider.

Table 1 lists only the external methods that developers use to manipulate a slider. The COREHTML5.Slider object also has many methods that it uses internally, such as initializeStrokeAndFillStyles() and createKnobCanvas().

Developers access the slider's value through its knobPercent attribute.


Using the slider

Listing 2 shows the HTML for the application shown in Figure 2.

Listing 2. The slider example's HTML
<!DOCTYPE html>
<html>
   <head>
      <title>Ad hoc components</title>

      <style>
         body {
            background: rgb(240,240,240);
         }
         
         #title {
            font: 18px Arial;
         }

         #slider-component {
            width: 400px;
            text-align: center;
         }

         #buttons {
            display: inline;
            font: 14px Arial;
         }
         
         #readout {
            margin-left: 25%;
            color: blue;
            font: 18px Arial;
            text-shadow: 2px 2px 2px rgb(255,255,255);
         }
         
         #slider {
            width: 75%;
            height: 30px;
            float: right;
         }

         .slider-button {
            background: rgba(100, 100, 100, 0.2);
            font: 24px Arial;
            font-weight: 1;
            border-radius: 4px;
            border: 1px solid rgba(100, 100, 180, 0.7);
            background: rgba(255, 255, 0, 0.2);
            box-shadow: 1px 1px 2px rgba(0,0,0,0.5);
            cursor: pointer;
            margin: 0px;
         }
      </style>
   </head>
   
   <body>
      <div id='title'>A custom slider</div>

      <p>
         <div id='slider-component'>
            <div id='controls'>
              <div id='buttons'>
                 <input type='button' class='slider-button'
                          id='minus-button' value='&ndash;'/>

                 <input type='button' class='slider-button' 
                          id='plus-button' value='&plus;'/>
              </div>

              <div id='slider'></div>
            </div>

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

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

</html>

The HTML in Listing 2 creates the DOM tree shown in Figure 3.

Figure 3. The slider example's DOM tree
The slider example's DOM tree, which contains a DIV with the ID of slider-component, which in turn contains two other DIVs, one with an ID of controls and another with an ID of buttons. The buttons DIV has two input elements, each of type button with a class of slider-button. One button has an ID of minus-button and the other has an ID of plus-button. Finally, the slider-component DIV contains an empty DIV with the ID of slider.

The HTML and CSS in Listing 2 is straightforward. The HTML references two scripts, one for the slider and another for the application itself. The application's script is shown in Listing 3.

Listing 3. The slider example's JavaScript
var slider = new COREHTML5.Slider('black', 'cornflowerblue', 0),
    readoutElement = document.getElementById('readout');

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

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

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

slider.addChangeListener(updateReadout);

slider.appendTo('slider');
slider.draw();

At the top of the JavaScript in Listing 3, the application creates the slider with a black stroke style, cornflower blue fill style, and an initial value of zero. At the bottom of the JavaScript, the application appends the slider to the DOM element whose ID is slider. In between, the JavaScript defines three event handlers that respond to button clicks and slider value changes.

The application adds onclick event handlers to the plus (+) and minus (-) buttons that adjust the slider's value (knobPercent), redraw the slider, and update the readout.

The application also adds a change listener to the slider that updates the application's readout to reflect the slider's new value. Components often provide a mechanism for registering event listeners and firing events to those listeners, and the slider component is no exception.

Now that you've seen how to use a slider, let's look at the slider component's implementation.


Creating and initializing the slider

Listing 4 shows the JavaScript code for the slider's constructor.

Listing 4. The slider's constructor
COREHTML5 = COREHTML5 || {};

COREHTML5.Slider = function(strokeStyle, fillStyle,
                            knobPercent, knobAnimationDuration) {
   knobPercent = knobPercent || 0;
   knobAnimationDuration = knobAnimationDuration || 1000; // milliseconds

   this.railCanvas = document.createElement('canvas');
   this.railContext = this.railCanvas.getContext('2d');
   this.changeEventListeners = [];

   this.initializeConstants();
   this.initializeStrokeAndFillStyles(strokeStyle, fillStyle);
   this.initializeKnob(knobPercent, knobAnimationDuration);

   this.createDOMTree();
   this.addMouseListeners();
   this.addKnobTransitionListeners();

   return this;
}

The first line of Listing 4 uses a common JavaScript idiom to ensure that a global object — in this case, COREHTML5— exists. (If it does exist, it's assigned to itself; otherwise, it's assigned to an empty object.)

The COREHTML5.Slider constructor function takes four optional arguments: stroke color, fill color, initial slider value, and knob animation duration in milliseconds. The knobPercent represents the slider's value.

The constructor creates a canvas —railCanvas— that contains the slider's rail. It also creates a second canvas —knobCanvas— with createKnobCanvas() (shown in Listing 5), which is invoked in Listing 4 by initializeKnob(). Finally, the constructor function creates the slider's DOM tree and adds listeners to the slider.

The first three slider methods invoked by the slider's constructor —initializeConstants(), initializeStrokeAndFillStyles(), and initializeKnob()— are shown in Listing 5.

Listing 5. The slider's initialization methods
COREHTML5.Slider.prototype = {
   initializeConstants: function () {
      this.SHADOW_COLOR = 'rgba(100, 100, 100, 0.4)';
      this.SHADOW_OFFSET_X = 3;
      this.SHADOW_OFFSET_Y = 3;
      this.SHADOW_BLUR = 4;

      this.KNOB_SHADOW_COLOR = 'rgba(255,255,0,0.8)';
      this.KNOB_SHADOW_OFFSET_X = 1;
      this.KNOB_SHADOW_OFFSET_Y = 1;
      this.KNOB_SHADOW_BLUR = 0;

      this.KNOB_FILL_STYLE = 'rgba(255, 255, 255, 0.45)';
      this.KNOB_STROKE_STYLE = 'rgb(0, 0, 80)';

      this.HORIZONTAL_MARGIN = 2.5 * this.SHADOW_OFFSET_X;

      this.VERTICAL_MARGIN = 2.5 * this.SHADOW_OFFSET_Y;

      this.DEFAULT_STROKE_STYLE = 'gray';
      this.DEFAULT_FILL_STYLE = 'skyblue';
   },

   initializeStrokeAndFillStyles: function(strokeStyle, fillStyle) {
      this.strokeStyle = strokeStyle ? strokeStyle : this.DEFAULT_STROKE_STYLE;
      this.fillStyle = fillStyle ? fillStyle : this.DEFAULT_FILL_STYLE;
   },

   initializeKnob: function (knobPercent, knobAnimationDuration) {
      this.animatingKnob = false;
      this.draggingKnob = false;

      this.knobPercent = knobPercent;
      this.knobAnimationDuration = knobAnimationDuration;

      this.createKnobCanvas();
   },

   createKnobCanvas: function() {
      this.knobCanvas = document.createElement('canvas');
      this.knobContext = this.knobCanvas.getContext('2d');

      this.knobCanvas.style.position = "absolute";
      this.knobCanvas.style.marginLeft = "0px";
      this.knobCanvas.style.marginTop = "1px";
      this.knobCanvas.style.zIndex = "1";
      this.knobCanvas.style.cursor = "crosshair";
      ...

   },
   ...
};

Separate canvases for the rail and knob

The slider component uses separate canvases for its rail and knob to facilitate using a CSS transition to animate the knob canvas when the knob's position changes. If the slider instead draws the rail and knob in a single canvas, that transition is not possible, because transitions can only be applied to HTML elements.

The initializeConstants() method creates variables for all the slider's constants, including defaults for the stroke and fill styles that initializeStrokeAndFillStyles() uses when the values are unspecified.

The most interesting method in Listing 5 is initializeKnob(), which sets some variables before calling createKnobCanvas() to create a separate canvas for the slider's knob. createKnobCanvas() creates a canvas element and sets its style attributes so the canvas is positioned above and to the top left of its enclosing canvas.

Now that you've seen how the canvases for the rail and knob are initialized, let's look at how they're used to draw the slider.


Drawing the slider

The slider component's draw() and erase() methods are shown in Listing 6.

Listing 6. Drawing and erasing the slider
COREHTML5.Slider.prototype = {
   ...

   erase: function() {
      this.railContext.clearRect(
         this.left - this.knobRadius, 0 - this.knobRadius,
         this.railCanvas.width  + 4*this.knobRadius,
         this.railCanvas.height + 3*this.knobRadius);

      this.knobContext.clearRect(0, 0, this.knobCanvas.width,
                                       this.knobCanvas.height);
   },

   draw: function (percent) {
      this.drawRail();
      this.drawKnob(percent ? percent : this.knobPercent );
   },
};

The erase() method erases both slider canvases — one for the rail and the other for the knob. Conversely, the draw() method draws the rail and the knob. You can pass the knob percent— the slider's value — to the draw() method, or you can call it with no arguments, in which case it will use the slider's existing value.

Drawing the rail

In Listing 7, the slider component draws the rail in two passes.

Listing 7. Drawing the rail
COREHTML5.Slider.prototype = {
   ...
   drawRail: function () {
      var radius = (this.bottom - this.top) / 2;

      this.railContext.save();
      
      this.railContext.shadowColor   = this.SHADOW_COLOR;
      this.railContext.shadowOffsetX = this.SHADOW_OFFSET_X;
      this.railContext.shadowOffsetY = this.SHADOW_OFFSET_Y;
      this.railContext.shadowBlur = this.SHADOW_BLUR;

      this.railContext.beginPath();
      this.railContext.moveTo(this.left + radius, this.top);
      this.railContext.arcTo(this.right, this.top, this.right, this.bottom, radius);
      this.railContext.arcTo(this.right, this.bottom, this.left, this.bottom, radius);
      this.railContext.arcTo(this.left, this.bottom, this.left, this.top, radius);
      this.railContext.arcTo(this.left, this.top, this.right, this.top, radius);
      this.railContext.closePath();

      this.railContext.fillStyle = this.fillStyle;
      this.railContext.fill();
      this.railContext.shadowColor = undefined;
      this.railContext.restore();

      this.overlayRailGradient();

      this.railContext.restore();
   },

   overlayRailGradient: function () {
      var gradient =
         this.railContext.createLinearGradient(this.left, this.top,
                                           this.left, this.bottom);

      gradient.addColorStop(0,    'rgba(255,255,255,0.4)');
      gradient.addColorStop(0.2,  'rgba(255,255,255,0.6)');
      gradient.addColorStop(0.25, 'rgba(255,255,255,0.7)');
      gradient.addColorStop(0.3,  'rgba(255,255,255,0.9)');
      gradient.addColorStop(0.40, 'rgba(255,255,255,0.7)');
      gradient.addColorStop(0.45, 'rgba(255,255,255,0.6)');
      gradient.addColorStop(0.60, 'rgba(255,255,255,0.4)');
      gradient.addColorStop(1,    'rgba(255,255,255,0.1)');

      this.railContext.fillStyle = gradient;
      this.railContext.fill();

      this.railContext.lineWidth = 0.4;
      this.railContext.strokeStyle = this.strokeStyle;
      this.railContext.stroke();
   },
   ...
};

First, the slider's drawRail() method fills the rail with a solid color, as shown in Figure 4.

Figure 4. Slider base
Drawing the slider base

Next, drawRail() overlays a white gradient, as shown in Figure 5.

Figure 5. Slider overlay
Drawing the slider overlay

The result, shown in Figure 6, gives the rail some depth and makes it appear as though a light is shining on it from the top.

Figure 6. Slider composite
Drawing the slider base and overlay

The slider's overlayRailGradient() method uses the HTML5 Canvas's createLinearGradient() method to create the gradient. Subsequently, overlayRailGradient() adds color stops at points along the gradient line. Each color stop is pure white with varying degrees of opacity. Finally, overlayRailGradient() fills the slider with the gradient and strokes its outline.

Drawing the knob

The knob, which the slider draws in a separate canvas, is shown in Figure 7.

Figure 7. The knob canvas
A screenshot of the slider's knob, showing the slider's color (rgba(255,255,0,0.5) and an abridged version of the function that draws the knob: createKnobCanvas(). That function creates the canvas element for the knob with this line of code: this.knobCanvas = document.createElement('canvas').

Recall from Listing 5 that the slider creates the knob's canvas with a call to document.createElement(). The slider's fillKnob() and strokeKnob() methods, shown in Listing 8, draw in that canvas.

Listing 8. Drawing the knob
COREHTML5.Slider.prototype = {
   ...

   drawKnob: function (percent) {
      if (percent < 0) percent = 0;
      if (percent > 1) percent = 1;

      this.knobPercent = percent;
      this.moveKnob(this.knobPercentToPosition(percent));
      this.fillKnob();
      this.strokeKnob();
   },
   
   fillKnob: function () {
      this.knobContext.shadowColor   = this.KNOB_SHADOW_COLOR;
      this.knobContext.shadowOffsetX = this.KNOB_SHADOW_OFFSET_X;
      this.knobContext.shadowOffsetY = this.KNOB_SHADOW_OFFSET_Y;
      this.knobContext.shadowBlur    = this.KNOB_SHADOW_BLUR;

      this.knobContext.beginPath();

      this.knobContext.arc(this.knobCanvas.width/2, this.knobCanvas.height/2,
                           this.knobCanvas.width/2-2, 0, Math.PI*2, false);

      this.knobContext.clip();

      this.knobContext.fillStyle = this.KNOB_FILL_STYLE;
      this.knobContext.fill();
   },

   strokeKnob: function () {
      this.knobContext.lineWidth = 1;
      this.knobContext.strokeStyle = this.KNOB_STROKE_STYLE;
      this.knobContext.stroke();
   },
   ...
};

The drawKnob() method takes a percent, which represents the slider's position. That method sets the slider's value and moves the knob accordingly, subsequently filling and stroking the knob.

The fillKnob() method fills the knob with a translucent yellow, which allows the rail underneath to show through and makes it appear as though the knob is lighted. The strokeKnob() method strokes the outline of the knob with a solid color.


Dragging the knob

The slider's code for dragging the knob is shown in Listing 9.

Listing 9. Dragging the knob
COREHTML5.Slider.prototype = {
   ...
   
   addMouseListeners: function () {
      var slider = this; // Let event handlers access this object

      this.knobCanvas.addEventListener('mousedown', function(e) {
         slider.draggingKnob = true;
         e.preventDefault();
      };
      
      this.knobCanvas.addEventListener('mousemove', function(e) {
         var mouse = null,
             percent = null;

         e.preventDefault();

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

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

      this.knobCanvas.addEventListener('mouseup', function(e) {
         e.preventDefault();
         slider.draggingKnob = false;
      }; 
   }, 

   windowToCanvas: function(x, y) {
      var bbox = this.railCanvas.getBoundingClientRect();

      return {
         x: x - bbox.left * (this.railCanvas.width  / bbox.width),
         y: y - bbox.top  * (this.railCanvas.height / bbox.height)
      };
   },

   knobPositionToPercent: function(position) {
      var railWidth = this.right - this.left - 2*this.knobRadius;
          percent = (position - this.left - this.knobRadius) / railWidth;

      percent = percent > 1.0 ? 1.0 : percent;
      percent = percent < 0 ? 0 : percent;

      return percent;
   },
   ...
};

Recall from Listing 4 that the slider's constructor calls the addMouseListeners() method shown in Listing 9. That method adds mouse-down, mouse-move, and mouse-up event handlers to the knob canvas that govern the dragging of the knob canvas. Two helper methods —windowToCanvas() and knobPositionToPercent()— make those event handlers easy to understand.


Next time

In this article you've seen how to implement an ad-hoc HTML5 component. The next article in this series continues the exploration of the slider component, showing you how to support change listeners, programmatically use CSS transitions to animate the slider's knob, and add the slider component to any DOM tree. See you next time.


Download

DescriptionNameSize
Sample codewa-html5-components-1.zip8KB

Resources

Learn

Get products and technologies

Discuss

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=858180
ArticleTitle=HTML5 components: Ad-hoc components, Part 1
publish-date=02192013