Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Tuesday, November 27, 2007

Standard Tabs

I was off last week, taking a much needed break. Of course, instead of really relaxing, I decided to play with a PHP framework that I have been tuning and instead laid the foundation for a web-based ’stuff’ management system (I have too many books and DVDs to remember what I own anymore). Anyway, this project gave me a good opportunity to build a website from the ground up, adhering to standards and accessiblity. Over the next few months, as I release parts of the project, I will be covering design/engineering topics related to this project.

Today, we will discuss the best-practices way to do a top navbar. So first, let’s state what is meant by best-practices in tab design:

  1. Scalable: both vertically and horizontally
  2. Simple, human-readable code
  3. Accessible
  4. Flexible: easy to change/update

The best HTML structure to meet this criteria is an unordered list, containing anchors that are the actual tab links. The unordered list can be easily (un)styled, is built with very little code, and is easy to change. Additionally, many screen readers will iterate through the list items. Here is some sample HTML code:

Example 1: Navigation HTML

<ul id=”nav”> <li id=”nav-home”><a href=”/”>Home</a></li> <li id=”nav-items”><a href=”/”>Items</a></li> <li id=”nav-add”><a href=”/”>Add</a></li> </ul>

The id attribute “nav” will be used to uniquely apply navigation styles to this list. If we ignore any tab background and border styles, you will need the following CSS:

Example 2: Navigation CSS

#nav { border-bottom: 1px solid #999; float: left; list-style: none; margin: 0; padding: 0; width: 100%; } #nav li { float: left; font-family: “Lucinda Grande”, sans-serif; /** change this to a font-family you like */ list-style: none; margin: 0; padding: 0; } #nav a { color: #666; display: block; float: left; font-size: 1.2em; font-weight: bold; padding: 0.6em 4em 0.6em 2em; /** This is the padding around the tab titles, modify it to fit your needs */ text-decoration: none; } #nav a:hover { color: #000; }

My page has odd padding, because I am using a variation of the slant tabs, as shown at Bulletproof Slants. The right side of the anchor tag has extra padding to accommodate the slanting background image. The tabs themselves will have a solid color when not hovering. In my variation the tabs, have a gradient when they are hovered, to pop them a bit. I am not a graphic designer, so my gradient sucks, but I believe it conveys the right idea.

Example 3: Navigation Background

#nav a { background: #CCC url(../img/nav/nav_slant.gif) no-repeat scroll right top; } #nav a:hover { background: #CCC url(../img/nav/nav_on_bg.gif) repeat-y scroll right top; }

Check out my example on my Tab Test Page;

An alternate strategy is described in Dan Cederholm’s book, Bulletproof Web Design, using two top to bottom gradient images, one fading to the non-selected color and the other fading to the selected color. Then swapping the background image when hovering.

posted by Matt Snider at 7:34 pm  

Friday, November 16, 2007

Convert Array-Like Objects To Arrays

I am attending a surprise party tonight and have a wedding to attend this weekend, so this will be a short post today.

In my recent post, Batch Function, I introduce a technique that can be used to turn arguments into Arrays. You use the Slice Function attached to the prototype of Array to convert Array-like objects into Arrays. For example:

// arguments var getArgumentsAsArray() { return Array.prototype.slice.call(arguments, 0); } // nodelist var getElementsByTagName(el, tagName) { return Array.prototype.slice.call(el.getElementsByTagName(tagName), 0); }

Mostly, I use this this technique to reduce elements from the front of a collection, by change the ‘0′ to the first index in the collection that I want. However, I recently realized that IE 6 does not support this technique on nodeLists (childNodes and lists returned by getElementsByTagName). In fact it will break you JavaScript code and throw an exception.

I played with various techniques to see if there was another Hack that worked for IE, but nothing did. In the end I came up with this x-browser safe function:

var convertToArray(obj, n) { if (! obj.length) {return [];} // length must be set on the object, or it is not iterable var a = []; try { a = Array.prototype.slice.call(obj, n); } // IE 6 and posssibly other browsers will throw an exception, so catch it and use brute force catch(e) { Core.batch(obj, function(o, i) { if (n <= i) { a[i - n] = o; } }); } return a; };

Where ‘n’ is the number of elements you want to skip. So, if I want to skip the first two elements of the collection, then “n = 2″, and if I do not want to skip any, then “n = 0″.

posted by Matt Snider at 6:09 pm  

Thursday, November 15, 2007

Web 2.0 Tabs

Jason Putorti of Mint.com and Novaurora, designed some cool graphical tabs for Mint. Although, they are not bullet proof (yet), they are very close. Angel Grablev reverse engineered this technique and wrote about it here:

Amazing CSS Tab’s for your website (mint.com inspired)

posted by Matt Snider at 7:05 pm  

Thursday, November 15, 2007

Font-Size Testing

I haven noticed that a lot of CSS Frameworks specify odd percentage amounts for various tags. For example, in my base.css, I set the H tags to these values (I believe I got them from either Tripoli or YUI):

h1 { font-size: 138.5%; } h2 { font-size: 123.1%; } h3 { font-size: 108%; }

Many times, the only explanation give is that after much testing, we determined that these were the optimal values for all browsers. I have always wanted to know a little more than that, so I created a test page that generates elements at a variety of font-sizes after applying reset.css. Using this tool, I have been checking different fonts on different browsers at a variety of sizes. I have not come to any conclusions yet, but you may find it valuable.

Font Size Test

It is a little slow, as I use JavaScript instead of a server-side language to insert the HTML elements, but it does the trick. I have been looking at the page in several browsers (FF2, IE 6 & 7, Opera 9, and Safari 2), then changing the zoom and/or relative font-size to see how well different percentages scale. Let me know if you discovery anything interesting ^_^.

posted by Matt Snider at 1:54 am  

Tuesday, November 13, 2007

Chris Heilmann on Unobtrusive JavaScript

This is a topic I mention fairly often, but have not taken the time to write an article about. Fortunately, there is a lot of good information already available on the web. Chris Heilmann of Yahoo, Inc. and owner of the blog “Wait till I come!” just posted his thoughts, which he summarized into 7 rules:

http://icant.co.uk/articles/seven-rules-of-unobtrusive-javascript/

Here are some other useful resources:

http://en.wikipedia.org/wiki/Unobtrusive_JavaScript
http://www.onlinetools.org/articles/unobtrusivejavascript/

The future of web programming is unobtrusive JavaScript, just look at the major portal companies and how they are separating JavaScript from design. If you have not invested the time to properly understand it, there is never a better time than now ^_^.

posted by Matt Snider at 12:33 am  

Sunday, November 11, 2007

Event Package Final

In case you have not been following the discussion so far, here are links to the other articles:

Event.js

Event Package on 11/07/07
Event Package Part 2 on 10/24//07
Event Package Part 1 on 10/18/07
X-Browser Event Handling on 09/28/07

Today, I will be walking through the Event.js, discussing the parts not previously covered. Let’s start by looking at the caching function:

Example 1: Cache Event Function

var cacheEvent = function(el, eType, fn, opt) { // ensure that opt in an object var o = opt || {}; o.capture = (o.capture || false); // wrap the function so we can control the scope and return the optional second object var wrapFn = function(e) { return fn.call(o.scope || window, e, o.data); }; // cache the listener so we can try to automatically unload var evt = [el, eType, fn, wrapFn, o]; ecache[ecache.length] = evt; return evt; };

This Function will be called any time an event is added using the ‘add’ method and accepts the same parameters as the ‘add’ method. The first part of the Function, ensures that the options exist and that the capture value is set. Capture is a required for the actual attachment of the event, but since it is false 99% of the time, we are making it optional. The ‘wrapFn’ internal Function is used to set the scope of, and pass through the data option into, the callback Function. We then create an array of the important values for the cache and push it onto the stack.

We return the cache object so that the ‘add’ Function can use ‘wrapFn’ as its callback and apply the value of capture. This creates an interesting problem when removing events using the old ‘remove’ Function, as we have attached the the ‘wrapFn’ as the callback, but code outside of this package, does not have access to the wrapping Function. That is why we use a private ‘remove’ Function, so that we can locate ‘wrapFn’ from the cache. The public ‘remove’ Function does just that:

Example 2: Public Remove Function

remove: function(el, eType, fn, fl) { var rs = that.getListeners(el, eType); Core.batch(rs, function(r) { if ((r.fn) == (fn)) { update(r); } }); },

The public remove Function first gets all the cached objects for a given element that match the event type, using the ‘getListeners’ method (discussed later). We then iterate on the results and compare the paremeter ‘fn’ with the cached non-wrapped Function. When this is true, we actually remove the event, otherwise. The update Function handles this removal:

Example 3: Removing Events and Cached Elements

var update = function(r) { remove(r.el, r.type, r.wfn, r.opt.capture); var arr = []; if (0 < r.index) { arr = ecache.slice(0, r.index); } arr.concat(ecache.slice(r.index + 1)); ecache = arr; };

First we actually remove the event with the internal ‘remove’ Fucntion (notice that the wrapped Function is used). The rest of the method slices the cached event object that we are removing out of the stack.

In order to quickly and effectively find the events attached on an Element, both internally and externally, we created the public ‘getListeners’ method. I model the data architecture after a similar Function in YUI, as I could not think of a more elegant way to return matching cache objects.

Example 4: getListeners Function

getListeners: function(el, eType) { var results = []; Core.batch(ecache, function(l, i) { if (l && l[EL] === el && (! eType || eType === l[TYPE]) ) { results.push({ el: l[EL], type: l[TYPE], fn: l[FN], wfn: l[WFN], opt: l[OPT], index: i }); } }); return results; },

We simply iterate through the cache and create special result objects each time the element, and event type when provided, match that of a cached event object. Because the results objects can be passed externally, where the position name variables (’EL’, ‘FN’, ‘TYPE’, ‘WFN’, and ‘OPT’) do not exist, we instead create objects where each member is the lowercase of the constant and is set to the comparable value. The index is added, so the event object can later be removed from the cache.

I added two other Functions that I found to be extremely useful. One is the ‘removeEvents’ Function that removes all events from a given element by leveraging the existing caching infrastructure from event removal. And a ‘removeAll’ Function that removes all events ever assigned using the ‘add’ method. This method is attached to the window unload event and helps prevent memory leaks (especially in IE6) where the memory reference to the dom element that triggers the event is not cleared, because the event is still pointing to the dom element.

Lastly, I removed as spaces from the Event name constants:

Example 5: Event Name Constants

BLUR: ‘blur’, CLICK: ‘click’, DOUBLECLICK: ‘dblclick’, KEYDOWN: ‘keydown’, KEYPRESS: ‘keypress’, KEYUP: ‘keyup’, LOAD: ‘load’, MOUSEDOWN: ‘mousedown’, MOUSEMOVE: ‘mousemove’, MOUSEOVER: ‘mouseover’, MOUSEOUT: ‘mouseout’, MOUSEUP: ‘mouseup’, SUBMIT: ’submit’, UNLOAD: ‘unload’,

I did this because there was a conflict between several events, such as the KEYUP event and the KEY_UP that actual keyboard key ‘up arrow’.

Many of the Function names have become shorter as well. I simply found that I knew when using the event package that ‘add’ meant that I was adding a listener, so I did not need the names to be longer.

Please let me know any feedback you have, especially if you have suggestions for improvement.

posted by Matt Snider at 4:46 am  

Friday, November 9, 2007

Support Multiple Safari Versions

Not enough time today to finish my event package article, but will do so tomorrow. Today, I wanted to share the multiple safari project. Many Mac designers have probably upgraded to Leopard and no longer have Safari 2. However, Safari 2 is still the dominant Safari browser and needs to be supported. The multiple safari project allows you to run different Safari instances in order to properly test each browser version.

http://michelf.com/projects/multi-safari/

posted by Matt Snider at 5:00 pm  

Wednesday, November 7, 2007

Event Package

I completed the event package that I have been working on (hurray).

Event.js

Basically, use Core.Util.Event.add to attach events and Core.Util.Event.remove to remove events.

I have also written a very rudimentary test page that I used to verify that event add/removal functions were all working properly with the internal cache.

Event Test

There are many improvements and there needs to be an in-depth article describing the package. However, I have to postpone the explanation until Friday’s post, when I am not so busy. Please take a look at Event.js and review it.

posted by Matt Snider at 1:12 pm  

Saturday, November 3, 2007

Batch Function

I introduced another pattern into the Event package and wanted to explain its mechanics, before introducing it. I attached this to my Core package (Core.batch).

A batch Function is a powerful tool used for replacing/simplifying ‘for’ loops. Anytime you are iterating on each element in an Array or Array-like structure, then you can use a batch function in its stead. A batch function will perform the iteration for you and pass the value of the element at index, the index, and any additional parameters into the function you provide it. Here is my current batch Function:

/** * Executes fn on each element in the set * * @method batch * @param set {array} the collection to iterate on * @param fn {function} the function to execute on each member of the collection; iterator function * @param {arguments} any number of arguments to pass into fn * @public */ batch: function(set, fn) { if (! set.length) {return;} var args = Array.prototype.slice.apply(arguments, [2]); args.unshift(null, null); // iterate on the items, executing the function, and stoping when function returns true or touched all childnodes for (var i = 0, j = set.length; i < j; i++) { args[0] = set[i]; args[1] = i; if (fn.apply(this, args)) { return; } } },

The first line ensures that the ’set’ is iterable. If it is not, then we return undefined. Next we leverage the ’slice’ method of the Array prototype on the arguments, which always returns an array, so any array-like Object (nodelists, arguments, etc.) becomes an array. We trim out the first two values of the arguments, which are the parameters: “set” and “fn”, and assign the result to ‘args’. Two empty elements are pushed into the front of the ‘args’. As we iterate through the set, we put values into the first two members of ‘args’ (0: value at index, 1: index). All other parameters passed into batch, those after ’set’ and ‘fn’, will also be passed into the iterator function as parameters using the apply method. The last part is to test the results of the iterator function and stop the batch process anytime it returns true. This way you do not always need to iterate on every member of the array.

Now, to see batch in action, suppose you have a ‘attachEventToSet’. This Function iterates through an Array and calls ‘attachEvent’ on each member of the Array.

attachEventToSet: function(set, eType, fn, capture) { for (var i = 0, j = set.length; i < j; i++) { Event.attachEvent(set[i], eType, fn, capture); } },

this could be simplified to:

attachEventToSet: function(set, eType, fn, capture) { Core.batch(function(el, i) { Event.attachEvent(el, eType, fn, capture); }); },

The definition of the inner Function inside of the attachEventToSet Function closure, provides access to the parameters: eType, fn, and capture. It you so choose, you could have also written the batch, as follows:

attachEventToSet: function(set, eType, fn, capture) { Core.batch(function(el, i, eType, fn, capture) { Event.attachEvent(el, eType, fn, capture); }, eType, fn, capture); },

Here, you are passing extra parameters through the batch Function and assigning them directly to the inner Function, instead of relying on the closure. Whenever possible, I usually use closures as the code is more concise.

Some possible areas of improvement are:

  • scope management - it may be useful execute the inner Function in a particular scope; I haven’t had a need to yet
  • validation - validate array and value of the array, also throw exceptions (or other error management) when bad parameters have been passed
  • quick functions - a small collection of common batch functions, such as comparison and update methods
posted by Matt Snider at 12:56 am  

Powered by WordPress