Contents


HTML5 components

Ad-hoc components, Part 1

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

Comments

Content series:

This content is part # of # in the series: HTML5 components

Stay tuned for additional content in this series.

This content is part of the series:HTML5 components

Stay tuned for additional content in this series.

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.)

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.
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 Related topics for a link to run the application; you can also download the source code.

Figure 2. A slider
Screenshot of 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.

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 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";
      ...

   },
   ...
};

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
Drawing the slider base

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

Figure 5. Slider overlay
Drawing the 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
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').
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.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

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