Expandable Widget Tutorial

Shiney Dashboard Widget Icon

After a year as “Dashboard Widget Engineer” for Apple Inc., I figure it’s past time for me to offer my first Dashboard Widget tutorial to the Googleshpere. So here goes…

Download Expandable Widget Sample Code.

Here lies an example of how to create a Growable/Shrinkable Widget in JavaScript, CSS and HTML. When clicked, the front of this Widget will expand to a predetermined size. When the widget is clicked again, it shrinks back to its original size.

If you’re on Mac OS X, you can view the example directly in any browser.

The difficult part of this example is creating a CSS-based layout that will allow pretty rounded-corner images to stay correctly positioned in their respective corners as the four sides of the widget grow. Not exactly rocket science, but this might be unfamiliar territory for you if you don’t hack a lot of dynamic CSS layouts.

To keep this example simple, I’m not going to create an actual Widget. Instead, I’ll show how to achieve the grow/shrink effect in a simple HTML page with CSS, JavaScript, and some standard Dashboard JS classes linked. I’ll debug and run the example directly in Safari (actually any modern browser for Mac OS X will do), and I’ll leave the incorporation of the example into an actual Widget as an exercise for the reader. ;-]

Start with a basic HTML or XHTML document including a doctype.

Doctypes are a hoary old technology that many consider to be poorly-designed and have caused much havok recently… but I recommend always including one in your HTML or XHTML. Why?

All modern browsers determine certain vital rendering behaviors according to the presence and flavor of doctype declared in your HTML. My advice is to always include a doctype in your HTML or XHTML document that will trigger Standards Mode. The absence of a doctype (or the presence of some older doctypes) will activate the so-called ‘Quirks Mode‘ which attempts to exhibit backwards-compatibility with many confusing rendering bugs from the early days of web rendering engines.

IMO, doctype switching kinda sucks, but there’s no denying we all have to live with it. Compared to Quirks Mode, Standards Mode always seems to exhibit layout behavior that makes more sense, is less surprising, and is more consistent across rendering engines. Developing against Quirks Mode (especially in multiple browsers) can be maddening, and I recommend avoiding it at all times.

Here I’ll use an HTML 4.01 Strict doctype which will trigger standards mode in all modern browsers.


<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
    <script type="text/javascript" src="/System/Library/WidgetResources/AppleClasses/AppleAnimator.js"></script>
    <script type="text/javascript" src="example.js"></script>
    <style type="text/css">
        @import "example.css";
    </style>
</head>
<body>

</body>
</html>

As you can see, the HTML file links to CSS and JavaScript files (exmaple.css and example.js), as well as the standard Dashboard AppleAnimator.js file which contains animation helper classes which are helpful (but not strictly necessary) for creating the animated resizing effect.

Let’s finish the HTML code before moving on to JavaScript or CSS. To create the growing and shrinking effect, compose a grid-like HTML/CSS layout with four corners, four edges, and one central area. The central area will expand in both width and height while the edges will each expand only in one dimension. Meanwhile, the corner divs will remain anchored in their respective crooks. Wrap the entire grid inside a div with an id of front which will serve as the front of the widget:


<div id="front" class="small" onclick="frontClicked(event);">
    <div id="topLef" class="corner"></div>
    <div id="topMid" class="edge"></div>
    <div id="topRit" class="corner"></div>

    <div id="midLef" class="edge"></div>
    <div id="midMid"></div>
    <div id="midRit" class="edge"></div>

    <div id="botLef" class="corner"></div>
    <div id="botMid" class="edge"></div>
    <div id="botRit" class="corner"></div>
</div>

The grid divs have appropriate ids and are divided into two classes by their type: corner and edge. These classes will make applying CSS styles to multiple elements more convenient later.

The front div also has a class of small to indicate that it will initially be in the small state and a click event handler to be defined later in example.js.

So much for the HTML… moving on to the CSS…

The first CSS task is to declare the size and position of the front div. Remember, this is the element whose size will change when clicked. The CSS rule for this div also contains a position:relative declaration… this will force all of the front div’s absolutely-positioned children to be positioned relative to it, rather than to the page as a whole. That will be necessary for the correct positioning of the corners during resizing.


#front {
    position:relative;
    width:176px; height:245px;
}

Next, declare common styles for both types of child div, corner and edge.


.corner {
    position:absolute;
    width:30px; height:30px;
    background-repeat:no-repeat;
}

.edge {
    position:absolute;
}

All edge and corner divs will be absolutely positioned. Also, all corners will have the same constant size: 30px by 30px.

Now the style rules for the corner divs. Each of these divs will contain a background image to add the illusion of pretty, round corners to the front of the widget. Also, each corner div will need two CSS declarations to anchor it to the appropriate corner of the front div. These two declarations will be some combination of top:0, left:0, bottom:0 and right:0.


#topLef {
    left:0; top:0;
    background-image:url(Images/topLef.png);
}

#topRit {
    right:0; top:0;
    background-image:url(Images/topRit.png);
}

#botLef {
    left:0; bottom:0;
    background-image:url(Images/botLef.png);
}

#botRit {
    right:0; bottom:0;
    background-image:url(Images/botRit.png);
}

Now that the corner divs are successfully anchored to their respective positions, it’s time to tackle the edge divs. The CSS rules for the edge divs are the essential trick that allow the expand/shrink effect to work.

Consider the top edge div. This div has an id of topMid. As the front of the widget expands, it will need to grow in width while its height remains constant. But the top div must also retain 30px margins on both the right and left sides. The 30px margins leave room for a corner div on each side.

The trick to achieving this behavior (which is essential to the entire example) is to specify both a left:30px and right:30px style declaration. This anchors the left and right sides of the top edge to the interior sides of the front div while leaving just enough space for the corner divs on each side. This produces the highly desirable situation that, as the front div grows, the top edge will naturally expand as well, without requiring any additional explicit scripting.

To anchor the top edge to the top of the front div, also add a declaration of top:0.


#topMid {
    left:30px; right:30px; top:0;
    height:20px;
    background-image:url(Images/topMid.png);
}

The three other edge divs are then given the same CSS treatment. The only difference is the edge to which they are anchored.

Finally, it’s time for the JavaScript code to produce the resizing animation of the widget. The JavaScript code will take advantage of the Dashboard standard AppleAnimator, AppleAnimation, and AppleRect classes to simplify the resizing animation, and will contain three functions:

  1. One event handler function to handle click events on the front div. This function will initiate the resizing animation.
  2. One animation callback function that will be repeatedly called by the AppleAnimator to set the current size of the widget during the animation.
  3. Another event handler function that will serve as the oncomplete handler for the AppleAnimation.

Start by defining the event handler function for click events on the front div. This function first checks the className property of the front div to determine the widget’s current state. Of course, this JS property maps to the class HTML attribute of the front div. Recall that this attribute was initially set to the value small in our HTML. This class attribute will store the current state of the widget and will need to be updated each time the resizing animation is begun and completed.

When the animation is started, I’ll change the front div’s class name to resizing, and when the animation is complete, the class name will be updated to small or large as appropriate.

Why is the resizing class name value needed? Imagine the widget being clicked in the middle of its resizing animation… this will result in jumpy and nasty behavior. Basically, the widget should ignore moues clicks while resizing, but respond to them at all other times. This effect could be achieved a few different ways, but here, I’ll use the transient resizing class name to indicate that mouse clicks should currently be ignored.


function frontClicked(evt) {
    var front = document.getElementById("front");
    if (front.className == "resizing") {
        return;
    }
    var isSmall = ("small" == front.className);
    front.className = "resizing";

Now that the current state of the widget is determined, create two AppleRect rectangles representing the small and large sizes of the front of the widget. Then determine which rectangle represents the starting and ending state of the animation depending on the front div’s current class name.


    var smallRect = new AppleRect(0, 0, 176, 245);
    var largeRect = new AppleRect(0, 0, 501, 416);

    var startRect = (isSmall) ? smallRect : largeRect;
    var endRect   = (isSmall) ? largeRect : smallRect;

Now use the standard AppleAnimator classes to control the resizing animation. Specify a duration of 500 milliseconds for the animation. Also specify that the animation callback should be called every 13 milliseconds:


    var animator = new AppleAnimator(500, 13);

Then create an AppleAnimation object with a start and end rectangle, and a pointer to the animation callback function resizeHandler which will be defined shortly. Also add the animation to the animator.


    var animator = new AppleAnimator(500, 13);
    var anime = new AppleRectAnimation(startRect, endRect, resizeHandler);
    animator.addAnimation(anime);

Finally, finish the function by attaching a oncomplete handler to the animator and starting the animation. Here I’ll use a closure to pass the state of the widget before the resizing animation began to the oncomplete handler. This is the clue that the oncomplete handler needs to set the front div’s class name after the resizing is complete.


    animator.oncomplete = function() {resizeCompleteHandler(isSmall)};
    animator.start();
}

Here’s the definition for the animation handler callback, resizeHandler:


function resizeHandler(anime, currRect, startRect, endRect) {
    var front = document.getElementById("front");
    front.style.width = currRect.right + "px";
    front.style.height = currRect.bottom + "px";
}

This simple function locates the front of the widget, and sets its current size to the current animation values.

And finally, the oncomplete handler. Remember that we used a closure to pass the state of the widget prior to starting the current resize animation to the oncomplete handler. resizeCompleteHandler simply uses this argument (wasSmall) to update class attribute of the front div:


function resizeCompleteHandler(wasSmall) {
    var front = document.getElementById("front");
    front.className = (wasSmall) ? "large" : "small";
}

See the Apple docs on the JS animation classes for more information.

And with that our example is complete. Happy Widgeteering!

Download Expandable Widget Sample Code.


About this entry