Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Wednesday, May 7, 2008

Visual Graphing in JavaScript

While on vacation, I took some time to have fun writing a JavaScript tool that I have never seen before. While on the plane, I decided to explore graphing in JavaScript, using the old-school TI-85 as a visual model. Even though, I never seen this done before, JavaScript is a fast language and the only reason it may have not already been written, is a lack of purpose. Nevertheless, I felt writing this application was fun exercise and an opportunity to explore writing a graphing program.

To start, we have to consider the how to draw the graph that we will be drawing lines on. We need to draw the x and y axises, and the tick marks on those axises. By default, I assumed that most people would be use a square DOM element, but it can be configured to use rectangular shaped elements as well. When instantiating a new instance of the Grapher, use the following:

Example 1: Instantiating Grapher

var graphNode = $(’graph’); var config = { equation: ‘-5 + Math.pow(x, 2)’, // default is ” tickFreq: 10, // default xRange: 10, // default yRange: 10 // default }; var graph = Core.Widget.Grapher(graphNode, config); graph.redraw();

In this example, the graph will range from -10 to 10 x and y, showing a tick my every other even value in those ranges, which happens to be the defaults assumed by the Object. To support this, the graphNode element need only be square; the Object will manage scale. We are also passing in a default equation so a line is initially drawn with the graph.

Inside the Object, we have several internal objects: ‘node’ (the graph DOM), ‘config’ (configuration of graph), ‘points’ (array of line points), and ‘that’ (the closure safe scope); 5 constants for the axis styles, point styles, and tick styles; and two helper Functions: one to clear the graph complete (’clearGraph’) and the second to only clear the points of the current line (’clearPoints’). The ‘clearGraph’ method removes everything from the graph DOM, and the ‘clearPoints’ method iterates through each point and removes them, while leaving the graph intact.

Example 2: Private Helper Functions

/** * Clears all graph related DOM nodes. * @method clearGraph * @private */ var clearGraph = function() { while (node.firstChild) { node.removeChild(node.firstChild); }; }; /** * Clears all graph point DOM nodes. * @method clearPoints * @private */ var clearPoints = function() { for (var i = points.length - 1; 0 <= i; i -= 1) { points[i].parentNode.removeChild(points[i]); } points = []; };

When the page first loads, or the graph DOM first becomes available, we need to make it look more like a graph. For this reason, there is a private method ‘drawGraph’, that calls ‘clearGraph’, then rebuilds the nodes that causes the DOM to look like a graph:

Example 3: Drawing the Graph

/** * Draws the actual graph: axis’ and ticks. * @method drawGraph * @private */ var drawGraph = function() { clearGraph(); var xAxis = node.appendChild($D.createTag(’div’, {style: XAXIS})); var yAxis = node.appendChild($D.createTag(’div’, {style: YAXIS})); Dom.setStyle(xAxis, ‘top’, config.region.top + config.yMid + ‘px’); Dom.setStyle(xAxis, ‘left’, config.region.left + ‘px’); Dom.setStyle(xAxis, ‘width’, config.width + ‘px’); Dom.setStyle(yAxis, ‘top’, config.region.top + ‘px’); Dom.setStyle(yAxis, ‘left’, config.region.left + config.xMid + ‘px’); Dom.setStyle(yAxis, ‘height’, config.height + ‘px’); for (var i = 0; i < config.tickFreq; i += 1) { var xTick = node.appendChild($D.createTag(’div’, {style: XTICK})); var yTick = node.appendChild($D.createTag(’div’, {style: YTICK})); Dom.setStyle(xTick, ‘left’, config.region.left + config.xTickStep * (i * 2) + ‘px’); Dom.setStyle(xTick, ‘top’, config.region.top + config.yMid - (config.tickFreq / 2) + ‘px’); Dom.setStyle(yTick, ‘left’, config.region.left + config.xMid - (config.tickFreq / 2) + ‘px’); Dom.setStyle(yTick, ‘top’, config.region.top + config.yTickStep * (i * 2) + ‘px’); } };

Once the DOM is clear, we create the x and y axises using the internal ‘config’ Object where we store many cached values about the DOM node and other values computed when the graph Object is instantiated. Both axises are lines that are positioned in the center of the node according to their respective dimension, and the height/width of the graph DOM node. Then we iterated through the tick frequency count and add the tick marks along the axises.

Finally, we need a method to draw lines according to a provided equation containing 1 or more ‘x’. For now, we simply eval the equation, but I intend to validate the equation to only support ‘+’, ‘-’, ‘/’, ‘*’, ‘%’, and all methods on the Math Object. Here is the method to draw the line:

Example 4: Drawing the Line

/** * Draws the line for the equation in a given color. * @method drawLine * @param eq {String} Required. The equation to draw. * @param color {String} Optional. The color of the line. * @private */ var drawLine = function(eq, color) { // iterate on the posible x points, stop when limit is reached or y exceeds grid for (var i = 0; i < config.width; i += 1) { var x = config.xStep * (config.xMid + (config.width > config.xMid ? -i : i)); var y = eval(eq.replace(/x/gi, -x).replace(’–’, ”)); var y2 = config.yMid - (y * config.yTickStep); // stop when exceeding graph range if (y > config.yRange || y < -config.yRange || isNaN(y)) {continue;}; var point = node.appendChild($D.createTag(’div’, {style: POINT})); Dom.setStyle(point, ‘left’, config.region.left + i + ‘px’); Dom.setStyle(point, ‘top’, config.region.top + y2 + ‘px’); if (color) { Dom.setStyle(point, ‘background-color’, color); } points[points.length] = point; } var k = points.length; // iterate on each point, determine if and fill all the y positions for (var j = 0; j < k - 1; j += 1) { var p1 = Dom.getRegion(points[j]); var p2 = Dom.getRegion(points[j + 1]); var useP2 = p2.top > p1.top; var diff = Math.abs(p2.top - p1.top); if (1 < diff) { for (var m = 1; m < diff; m += 1) { var left = m < diff / 2 && useP2 ? p1.left : p2.left; var top = (useP2 ? p1.top : p2.top) + m; var p = node.appendChild($D.createTag(’div’, {style: POINT})); Dom.setStyle(p, ‘left’, left + ‘px’); Dom.setStyle(p, ‘top’, top + ‘px’); if (color) { Dom.setStyle(p, ‘background-color’, color); } points[points.length] = p; } } } };

The method requires an equation and also excepts a color, if you wish to not use black to draw a line. This is forward thinking, so that, once I figure out the best way to do it, each graph will be able to support multiple lines. We iterate through each 1px in the width of the graph DOM node. So we first compute ‘x’ (value of x at pixel i), ‘y’ (value produced by the equation), and y2, the actual top position of the point. These values are rather tricky, because computing x changes depending on whether we are on the left or right of the y-axis, and computing y is done using eval and regex replaces. I learned that the equation ‘-x’ would throw an error computing ‘y’, because it had double negatives, so I fixed that using a simple regex.

When iterating, we ‘continue’ when the value of ‘y’ does not fit inside the configured range, then we create the point DOM node and position it (we would also color it, if a color was provided), and cache the point. Then we iterate through the points and fill in each pixel between point n and ‘n - 1′ when they are more than 1-pixel apart in the y-axis, creating a smooth line. Whichever point is higher determines which point to use as the top offset, and since each point is only 1-pixel separated on the x-axis, we also use the higher point to determine which pixel should be used for the ‘left’ style, depending on how close the fill point is to the point used for the ‘top’ style.

When you bring it all together, you get something like this example: Graph Test. I plan on making the graph colors more customizable, cleaning all equation strings to ensure no tomfoolery, additional graph stuff (like titles, legends, labels, etc.), and supporting multiple lines on a single graph.

posted by Matt Snider at 11:43 pm  

Sunday, April 27, 2008

removeNode and appendNode Methods

First, for everyone following the JavaScript Game Engine project, I am putting it on hold for now. Building an engine to support dynamic games requires a lot of work… a lot more more time than I currently have to devote to the project, and I do not want to release code of questionable quality. I will, however, revisit it from time to time and complete the article series as I am able.

For todays article, I would like to introduce two of my favorite DOM extension methods, “removeNode” and “appendNode”. They are basically, simple wrappers for “removeChild” and “appendChilde” with the added functionality to animate the addition/removal of a DOM nodes. I find these methods to be very useful in providing feedback to a user when they are taking an action that modifies the DOM. Most of the time (and the default animation of this method) an opacity fade does the trick, making the addition/removal of a DOM node tangible to the user, but sometimes you need a different animation, so the methods accept animation arguments as optional parameters. I build these methods on top of YUI animator and either add them to the “YAHOO.util.DOM” or Document Object.

Example 1: AppendNode

Document.appendNode = function(root, elem, ap, fn) { var node = $(elem), parent = $(root); if (parent && node) { parent.appendChild(node); var animParams = (isType(ap, ‘object’)) ? ap : {opacity: {from: 0.25, to: 1}}, anim = new YAHOO.util.Anim(node, animParams, 0.5, YAHOO.util.Easing.easeIn); anim.onComplete.subscribe(function() { if (fn && isType(fn, ‘function’)) {fn(node);} }); anim.animate(); } return node; };

The “appendNode” method has two required parameters: the ‘root’ element to append to, and the ‘node’ to append with. As usual, I have written the “YAHOO.util.Dom.get” method as the ‘$’ shortcut method. By default the method will animate with a fade in from 0.25 opacity to 1 opacity, but you can override it by passing in your own arguments for YUI animation. If you want an event to occur after the animation, then provide a Function as the last argument, however it is also not required; this Function will be passed the newly appended ‘node’ as its only parameter.

Example 2: RemoveNode

Document.removeNode = function(elem, ap, fn, isRemoveListeners) { var node = $(elem), parent = node.parentNode; if (parent) { var animParams = (isType(ap, ‘object’)) ? ap : {opacity: {from: 1, to: 0.25}}, anim = new YAHOO.util.Anim(node, animParams, 0.5, YAHOO.util.Easing.easeOut); if (isRemoveListeners) {Event.purgeElement(node, true);} anim.onComplete.subscribe(function() { parent.removeChild(node); if (fn && isType(fn, ‘function’)) {fn();} }); anim.animate(); } };

The “removeNode” method is very similar to the “appendNode” method, with the noted exception that the animation must occur before the actual removal of the child-node. You do not need to pass in the ‘root’ Element as we can extract that directly from the provided node. Lastly, if you have attached any listeners to the node or its children, you should probably remove them. I leveraged YUIs “YAHOO.util.Event.purgeElement” (shortcut name of “Event.purgeElement”) method for this, which will recursively remove the events attached to your node and its children, when you pass true as the last parameters (isRemoveListeners), otherwise no events will be removed.

Again, these methods are simple, but very handy. I have found it is almost always useful to provide feedback to users when they are adding/removing DOM nodes, and these methods abstract the logic away, so that I can do just about anything I need. If you’d like to give them a try, I have provided a test page.

posted by Matt Snider at 12:34 pm  

Sunday, April 20, 2008

Game Menu

For the game project, I felt it would be prudent to have a menu that handles the control of the game engine. For this task, I have decided to use YUI Menu, because I know YUI well, and it is powerful, extendable, and easy. One of the great things about the YUI is the plethora of examples about how to use it, including the menu widget. You can also reference the JavaScript directly from the YUI site, so there is no need to maintain them yourself, or worry about versions.

Example 1: Files Needed For YUI Menu

<link rel=”stylesheet” type=”text/css” href=”http://yui.yahooapis.com/2.5.1/build/menu/assets/skins/sam/menu.css”/> <script type=”text/javascript” src=”http://yui.yahooapis.com/2.5.1/build/yahoo-dom-event/yahoo-dom-event.js”></script> <script type=”text/javascript” src=”http://yui.yahooapis.com/2.5.1/build/container/container_core-min.js”></script> <script type=”text/javascript” src=”http://yui.yahooapis.com/2.5.1/build/menu/menu-min.js”></script>

Since, YUI does such a good job documenting how to use their widgets, there isn’t a need to say much about them. The Yahoo developer site has examples showing how to create menus from markup, JavaScript, or some combination of both. I choose the last option, using markup to design the root menu, and JavaScript to build the submenus. You need to apply the class “yui-skin-sam” to the body tag and various other classes to your menu widget, depending on what type of menu you are making. I choose the “Application Menubar” styles, positioning the widget statically at the top left of the page. Give my menu a try:

Game Menu

One important thing to note, is that if you dynamically updating the menu items, as I do when checking and disabling menu items, make sure that lazy-loading is set to false, otherwise, you will find that parts of your menu are undefined.

My menu is still very basic, but does most of what I expect I will want for the game engine. You can start a new game or restart an existing one, change the resolution, or exit the game. Starting a new game will bring up an empty game board, sized 640 X 480, while restart will bring up a new, empty game board that is the same size as the existing window. I am leveraging the ‘disabled’ flag for the menu, so that certain options, such as resolution and restart are not available until the game has started. I also use the ‘checked’ flag, so that you the resolution menu shows which resolution is selected. The view -> resolution menu item allows you to resize the gameboard to the various supported sizes for the game (right now I threw in some test sizes). The exit game doesn’t do anything for now. I’m not sure what it really will do, unless I implement some type of login feature. Lastly, you can add help text to the menus, which I have used to indicate what possible shortcut keys trigger the menu items, although I haven’t wired them up yet.

posted by Matt Snider at 12:49 pm  

Tuesday, April 15, 2008

Document CreateTag Method

As I worked on the GameEngine improvements, I realized that it would be nice to have a menu and several other DOM elements. Since the engine will be standalone, it is necessary to dynamically create the DOM elements required to run the game. For this task, I have use a method “document.createTag()”, which I often refactor for various projects. In its most basic form, it looks like the following:

Example 1: document.createTag

YAHOO.lang.augmentObject(document, { /** * Creates and returns an html element and adds attributes from the hash. * * @method createTag * @param tagName {String} Required. Tag name to create. * @param h {Object} Optional. The hashtable of attributes, styles, and classes; defaults is empty object. * @return {Element} The newly created element; returns null otherwise. * @static */ createTag: function(tagName, h) { var Dom = YAHOO.util.Dom, node = document.createElement(tagName), hash = isType(h, ‘object’) ? h : {}; // iterate through the possible attributes for (var k in hash) { var v = hash[k]; k = k.toLowerCase(); if (isType(v, ’string’) || (’style’ === k && isType(v, ‘object’))) { switch (k) { case ‘classname’: case ‘klass’: case ‘class’: case ‘cls’: Dom.addClass(node, v); break; case ‘cellpadding’: node.cellPadding = v; break; case ‘cellspacing’: node.cellSpacing = v; break; case ‘colspan’: node.colSpan = v; break; case ‘checked’: case ‘disabled’: node[k] = v; break; case ‘rowspan’: node.rowSpan = v; break; case ’style’: // iterate through the style object for (var t in v) { var s = v[t]; if (isType(s, ’string’) || isType(s, ‘number’)) { Dom.setStyle(node, t, s); } }; break; case ‘innerhtml’: case ‘text’: if (isType(v, ’string’) && ! v.match(/< .*?>/) && ! v.match(/&.*?;/)) { node.appendChild(document.createTextNode(v)); } else { node.innerHTML = v; } break; default: node.setAttribute(k, v); break; } } }; return node || null; } });

To use this method, simply include it in your JavaScript source files, then call “document.createTag” passing in two parameters: 1st parameter is the tag name you wish to create, and the 2nd is an Object where the keys are tag attribute names and the values are the attribute values. Using a ‘for … in’ loop we iterate on each key in the Object, evaluating only those values that are strings and numbers, or an object in the case of the style attribute. The switch statement then, normalizes the keys, so the key ‘checked’ does the same thing as ‘cHeCkEd’, because capitalization does matter.

Over the years I have added each case of the switch statement based on issues experienced in various browsers. Most attributes seem to be applied just fine, when you use the ’setAttribute’ method, so this is the default case. Some attributes such as ‘checked’ and ‘disabled’ do not work in all browsers with ’setAttribute’, so they are applied only with dot notation. Some attributes such as ‘cellPadding’ and ‘cellSpacing’ are case-sensitive in certain browsers (and capitalizing them doesn’t break the browsers that lower-case worked in), and/or can only be applied via dot notation, so we special-case each of them. The word ‘class’ is a reserved word in at least IE, so you cannot do “object.class”, instead you need to use “object.className”, and this seems to be supported by all browsers; I also support various ways of storing the classname as a key, because different projects have different naming conventions. Style is a special-case that must contain an object, where the keys are style names, and the values are what you want to set the style to (strings and integers are acceptable). Lastly, the ‘text’ or ‘innerhtml’ key will attempt to append the provided value as a text node, unless the value: isn’t a string, contains HTML elements, or contains HTML special characters; in those cases it uses innerHTML, because it will render HTML and special characters correctly, while a text node will simply print the text as is.

Now, this method probably isn’t perfect, but it works with all the browsers that I have ever needed to support. Feel free to chime in, if you know of any other special-case situations or browsers that you believe this method won’t work with.

This solution is homegrown, and there may exist better DOM node creation methods, with more exposure and support. I know the Framework Ext.js has a DOM node creation method, and I wouldn’t be surprised if Dojo or jQuery have a plugin solution as well.

posted by Matt Snider at 9:46 pm  

Monday, April 14, 2008

Sliding Puzzle Game Using Movable

So, as promised, I will be showcasing a game today that makes use of the Movable Object from last Tuesdays article. I had hoped to get this article out earlier, but my old game engine was too sloppy to use. Instead, I hacked together a new engine, but it is ‘hacked’ together and will currently only work with this game. I intend to experiment with the engine more this next week to see how abstract I can make it, or at least make each element extendable, and fire and listen for Custom Events.

The game is called Sliding Puzzle, this particular variant is called The Fifteen Puzzle. The game objective is to move 15 randomly placed tiles into and out of the one open space on a 4×4, 16-space board (there is 1 open space), until you have arranged them in order. I originally played this game on the 8-bit Nintendo Console, as The Fifteen Puzzle was a hidden, side-game in Final Fantasy One.

To make the game work, I wrote 3 new classes: GameEngine, GameBoard, and GameTile. The GameTile Object will manage the tiles, which wraps the Movable Object and needs to know its position on the board. The GameBoard manages all the tiles, handling tile movement and test for game completeness (win or lose states). The GameEngine should handle pre/post game user interaction, and initialize and manage the other objects. I also improved the Movable Object so that it can be limited to move about a DOM node or the viewport.

Unfortunately, much of the code is still hacked together and the Objects know too much about each other. I intend to work on cleaning the code up this week, but until then, here is a Fifteen Puzzle Game Demo.

posted by Matt Snider at 12:16 am  

Tuesday, March 25, 2008

Session Driven Back

One of the biggest issues I encountered while refactoring my projects to work without the need of JavaScript, is directing the user back to the page they were previously on. This is especially important for form submission and canceling. I look around the web and mostly saw comments directing programmers to use JavaScript to handle back. However, the point of the exercise is to not require JavaScript for anything.

I then spent some time looking at Server Frameworks (like Spring and Rails) that include form managers, before I came up with a light-weight, session-driven solution (example written in JAVA) that I am now using on my projects. These two methods should be added to your base controller, or servlet equivalent, that has access to HttpServletRequest:

Example 1: Server History Code

private void setupCache(HttpServletRequest request) { Boolean noCache = “T”.equalsIgnoreCase(request.getParameter(request, “noCache”)); Integer cacheMaxLength = 3; if (! noCache && “get”.equalsIgnoreCase(request.getMethod())) { List<String> h = (List<String>) request.getSession().getAttribute(”HISTORY”); String url = request.getRequestURI() + “?” + request.getQueryString(); // ensure history cache exists if (null == h) { h = new ArrayList<String>(); } // remove cache greater than max size else if (cacheMaxLength < h.size()) { h.remove(0); } // prevent double caching on refresh if (0 == h.size() || ! h.get(0).equals(url)) { h.add(url); request.getSession().setAttribute(”HISTORY”, h); } } } protected String getHistory(int i) { List<String> h = (List<String>) request.getSession().getAttribute(”HISTORY”); if (null == h || i >= h.size()) {return null;} return h.get(h.size() - i - 1); }

All you need to do is call “setupCache” at the beginning of all your controllers that you want in the history, and then call “getHistory” with the index of the history item you wish to forward to (1 is the last page) to retrieve that pages URL. If you ever want a page to not cache, then pass it the query parameter “&noCache=T”. I also don’t cache “POST” requests, because they are form submission. Assuming neither of these conditions are true, we then add the current URL to the List object that holds our history on the session (mapped as ‘HISTORY’).

When adding to the cache, we limit the cache size to 3, because you do not want the session object per user to grow indefinitely, and I generally only care about the last page. However, you could easily increase this to 5 or 10 by replacing the constant value of “cacheMaxLength”. Lastly, there is no reason to cache the same URL twice, as this usually happens when the user is refreshing the same page, so we ignore duplicate URLs.

Retrieving a URL from the history cache, is as easy as calling “getHistory” with an index; generally, 0 = current page, 1 = last page, 2 = page before last, etc. To do a redirect, you can simply do the following:

Example 2: Response Redirect

String url = getHistory(1); response.sendRedirect(url);

So to summarize, all your view requests are cache by their URL, but only the current and last 2 pages, and you can prevent a page from being cached by passing the “noCache” parameter equal to “T”. In order to retrieve the last page, simply call “getHistory(1)” for the URL.

posted by Matt Snider at 10:56 am  

Tuesday, March 18, 2008

Triggering A Popup On Unload

First, let me say that there are very few good reasons to ever use this technique and the last thing I want is to see the re-institution of the popup mania that plagued the web before the advent of the popup blocker. However, you might need to stop the user before they leave your site to inform the user of: unsaved data, an exit survey about your product, something they may have earned/won (I mean really won something, not one of those sign up for 6 things and get an ipod scams), etc. Keep in mind that popups should be used sparingly, as: most people find them annoying, many browsers will automatically block them, and users generally distrust sites with popups. That said, let me explain how best to trigger a popup onunload.

First, most browsers now automatically block popups when not triggered by an anchor tag event, with the exception of the IEs of the world, as any popup not fired by a user action (onclick) are deemed to be malicious. That means that you should inform your users to disable their popup-blockers for your site and let them know that what to expect, or few non-IE users to ever see the popups. That said, the two JavaScript native popups, alert and confirm, will never be blocked and should be used as a first solution whenever possible. For example, suppose you want to inform the user that they have unsaved data:

Example 1: Using Confirm

YAHOO.util.Event.addListener(window, ‘beforeunload’, function(e) { if (! confirm(’You have unsaved data on this page, leave this page? Clicking “yes” to continue will loose your changes forever.’)) { YAHOO.util.Event.stopEvent(e); } });

To let users know that they have unsaved data, you probably never need anything more than that. However, what if you have a web-service, receiving thousands of unique visitors a day, but only a fraction of those are actually signing up for your service. The biggest question you would have is, what can I do better to reach these potential users. In this case you might consider an exit survey:

Example 2: Exit Survey

YAHOO.util.Event.addListener(window, ‘beforeunload’, function(e) { window.open(’urlofyoursurvey’, ’surveywindow’, ’status=off,toolbar=off,location=off,menubar=off,directories=off,resizable=off,scrollbars=off,height=800,width=800′); });

FYI: A great, free service for making quick, custom surveys is Survey Monkey.

However, like I said before, this is going to be blocked by the vast majority of browsers. But, did you know there is a better way that most browser will not block:

Example 3: Exit Survey on Anchors

var allowedDomains = [’mattsnider.com’]; var isAllowedDomain = function(href) { for (var i = allowedDomains.length - 1; 0 <= i; i -= 1) { if (-1 < href.indexOf(allowedDomains[i])) { return true; } } return false; }; var showPopup = function() { window.open(’urlofyoursurvey’, ’surveywindow’, ’status=off,toolbar=off,location=off,menubar=off,directories=off,resizable=off,scrollbars=off,height=800,width=800′); }; YAHOO.util.Event.addListener(document, ‘onclick’, function(e) { var targ = YAHOO.util.Event.element(e); if (targ.href && ! isAllowedDomain(targ.href) { showPopup(); } } });

In Example 3, we are listening to all clicks on the document, but ignoring everything that is not an anchor tag. The ‘allowedDomain’ Array and ‘isAllowedDomain’ test Function are provided for the case where you do not want to show your exit survey if the user navigates to another page of your site (in my case all urls with the domain ‘mattsnider.com’). Because we have attached the event to onclick, most browsers will not block this popup, giving you greater visibility among different user segments. However, this technique misses people who navigate away from your site using any means other than clicking on a link. To capture those users you will need to attach an event to the onunload:

Example 4: Exit Survey Onunload

YAHOO.util.Event.addListener(window, window.onbeforeunload ? ‘beforeunload’ : ‘unload’, function(e) { if (0 > YAHOO.util.Event.getPageY(e)) { showPopup(); } });

First, we are attaching a listener to ‘onbeforeunload’ or ‘onunload’, depending on whether the browser supports ‘onbeforeunload’ or not. In the callback function we test to see if the Y-coords of the unload event are less than ZERO, because most browsers will return a negative number if the unload event is triggered by any user action above the actual page, but still within the browser (back/forward, changing the url, closing the window/tab, etc.). This way, we won’t double trigger the popup if the user leaves the page by clicking on a link. However, Example 4, will be blocked by most popup blockers, and is it is just a fail-safe to try and reach the largest user segment possible.

Lastly, remember not to abuse this knowledge. Please do not place gratuitous popups on your pages (and if you do, definitely, don’t credit me for them). Also, keep in mind that I have used YUI for this example, but only for the Event handling methods, which could be easily replaced with a Simple Event Package or your favorite JavaScript Framework.

posted by Matt Snider at 9:12 am  

Saturday, March 1, 2008

Using For In Loops Safely

Many of the JavaScript Frameworks and Libraries used today have the bad habit of attaching methods to the Prototype of Array and Object. While, in theory it would be really nice to be able to extend these Object, in practice, doing so breaks “For … in” loops. For example, if you include Json.js” in your project, you have the power to easily work with JSON objects. However, it attaches “toJSONString” method to “Array.prototype”. So when you use the “For … In” loop on any array, one of the keys will be the bogus key “toJSONString”.

This is frustrating as “For … In” loops are the only way, in JavaScript, that you can iterate on the keys of an Object and associative Arrays. Most Frameworks that modify these objects also add functions that allow you to work around this limitation, but then you become dependent on them to do any Array iterations. Douglas Crockford (see JSLint) and other JavaScript guru’s recommend keeping a list of keys that you want to ignore and always filtering your “For … In” loop with it. This is one way to fix the problem, but what does one do when they don’t know the keys or Array/Object prototypes change dynamically as your JavaScript executes, such as when you use YUI to extend Object and it attaches the “constructor” variable to Object’s prototype.

Often you need the ability to dynamically determine what keys to filter before each “For … In” loop. To accomplish this, I have developed the following function:

Example 1: GetKeysToIgnore

var getKeysToIgnore = function() { var tarr = [], // empty array tobj = {}, // empty object tdom = document.getElementsByTagName(”body”), // empty nodelist keys = {’item’: true, ‘length’: true};// default keys to ignore // iterate on the native array object for (var k in tarr) {keys[k] = true;} d // iterate on the native object object for (var l in tobj) {keys[l] = true;} // iterate on the nodelist, but don’t ignore indices for (var m in tdom) { if (isType(parseInt(m, 10), ‘number’)) {continue;} keys[m] = true; } return keys; };

This method creates an instance of each of the three type of iterable JavaScript objects: Array, Object, NodeList (especially important of Safari). Since, these objects are empty, they contain only the keys for elements that have been attached to their Prototypes. Therefore, when we iterate on it we can create a hash Object of all these keys. The default list is just some keys that you want to reserve for your project, which may or may not always be attached to one of these 3 objects.

Call “getKeysToIgnore” before any “For … In” loop and then filter these keys out:

Example 2: Filtering

var test = { key1: 1, key2: “1223′, key3: new Date() }; var ikeys = getKeysToIgnore(); for (var key in test) { // key is not in our ignore hash if (! ikeys[key]) { // put your logic here } }

Using this technique, you could modify the “batch” Function of Core.js to use this technique, thereby supporting both Array and Object iteration:

Example 3: Batch

Core.batch = function(o, fn) { var args = Array.prototype.slice.apply(arguments, [2]); var ikeys = getKeysToIgnore(); args.unshift(null, null); // iterate on the items, executing the function, and stoping when function returns true or visited all elements for (var key in o) { // key is not in our ignore hash if (! ikeys[key]) { args[0] = o[i]; args[1] = i; var rs = fn.apply(this, args); if (rs) { return rs; // allows the batch function to return a found result } } } };

While this is a little more powerful than the Array based “batch” Function, it is a little slower because the ikeys array must be created each time before batching. Something to think about is how to improve/cache getKeysToIgnore better and increase the performance of this Function.

posted by Matt Snider at 12:46 pm  

Sunday, January 27, 2008

Date Functions

Like String, the native JavaScript Date object also does not have as many helper functions as you might like. Using a similar approach as we did with String, we can attach additional functionality to Date.

Example 1: Date Augmentation

YAHOO.lang.augmentObject(Date, { /** * Date constant for full month names * * @property MONTHS * @type string */ MONTHS: [’January’,'February’,'March’,'April’,'May’,'June’,'July’,'August’,'September’,'October’,'November’,'December’], /** * Returns a date object from the string; expects ‘MonthName, DayNr Year Hrs:Min:Sec’, may not work properly on other strings in all browsers * * @method getDate * @param s {string} the date as a string * @return {date} a date object, defined by the passed string * @static */ getDate: function(s) { var d = new Date(); d.setTime(Date.parse(s)); return d; } }, true);

I try to only attach constants and static methods to the JavaScript Native Object (Date) itself. Dynamic objects/methods should be attached to the prototype of the Object (Date.prototype) so that all instantiations of that Date have access to them. In example 1, we attach a constant array (the full American month names) and one method ‘getDate’ (retrieves the date from a string). The constants can be used to retrieve the month name for a date, a feature that is missing from JavaScript, most likely because of localization concerns. ‘getDate’ is a nice shortcut, when you have a date string (say from an AJAX request) and need to convert it to a Date object for manipulation.

Example 2: Date.prototype Augmentation

// Extending the Date.prototype Object YAHOO.lang.augmentObject(Date.prototype, { /** * Retrieves the name of the month * * @method getMonthName * @return {string} the month name * @public */ getMonthName: function() { return Date.MONTHS[this.getMonth()]; }, /** * Retrieves the abbreviated name of the month * * @method getMonthNameAbbr * @return {string} the abbreviated month name * @public */ getMonthNameAbbr: function() { return this.getMonthName().substr(0,3); }, /** * Converts the JavaScript Date into a date string. Recognizes the following format characters: y = year, m = month, d = day of month, h = hour, i = minute, s = second * * @method toDateString * @param (string} format OPTIONAL: The string format to convert the JavaScript Date into (ie. ‘m/d/y’ or ‘m. d, y’); default is ‘m/d/y’ * @param {boolean} showZeros OPTIONAL: Forces trailing zeros, so 9/1/2006 becomes 09/01/2006 * @param {Mixed} useMonthName OPTIONAL: string or boolean, use the month name instead of the digit, (’abbr’ uses the short name) * @return {string} the JavaScript Date as a string * @public */ toDateString: function(format, showZeros, useMonthName) { if (! isType(format, ’string’)) {format = ‘m/d/y’;} format = format.toLowerCase(); // cast all values to strings var day = ” + this.getDate(), month = ” + (this.getMonth() + 1), hour = ” + this.getHours(), minute = ” + this.getMinutes(), second = ” + this.getSeconds(), year = ” + this.getFullYear(); // pad leading zeros if (showZeros) { if (1 === day.length) {day = ‘0′ + day;} if (1 === month.length) {month = ‘0′ + month;} if (1 === hour.length) {hour = ‘0′ + hour;} if (1 === minute.length) {minute = ‘0′ + minute;} if (1 === second.length) {second = ‘0′ + second;} } // use month name if (useMonthName) { month = (isType(useMonthName, ’string’) && ‘abbr’ === useMonthName.toLowerCase())? this.getMonthNameAbbr(): this.getMonthName(); } return format.replace(’y', year) .replace(’d', day) .replace(’h', hour) .replace(’i', minute) .replace(’s’, second) .replace(’m', month); // do month last as some months contain reserved letters }, /** * Converts JavaScript Date into a MySQL dateTime string ‘1969-12-31 00:00:00″ * * @method toTimeString * @return {string} the JavaScript Date as a MySQL time string * @public */ toTimeString: function() { return this.toDateString(’y-m-d h:i:s’, true); } }, true);

These are the few methods that I find immensely helpful for almost any project. The first two return either the month name or the abbreviated month name of the date. I first used this for a content management system and often find a need for them. ‘toDateString’ is one of my favorite functions and it converts a Date object into most useful date strings. I was writing a lot of PHP at the time and modeled it after the ‘date’ Function in PHP (it needs some work as it was written ages ago). Ideally, I could find the PHP source code and model my JavaScript objects after it, but this method has still been very useful anytime I needed to insert a Date from JavaScript into the page. The last method ‘toTimeString’ allows you to pre-format your MySQL date times, before sending them to the back-end… most languages will do these conversions for you, but I have occasionally found it useful (remember to always validate any parameter on the back-end before using one in a database query).

If you are looking for more functionality, YUI has a huge collection of date math static function in Calendar that you could also attach to the “Date.prototype”.

posted by Matt Snider at 11:39 pm  

Friday, January 18, 2008

String Functions

Strings in JavaScript do not have as many helper functions as one might like. As a result you will probably need to write a collection of methods yourself. You can either create a static utility Function or extend the “String.prototype”. String is one of the few objects that it is fine to extend the prototype object of, as you do not need to use “for … in” on strings.

For our discussion today, I will be using YAHOO.lang.augmentObject to augment the String.prototype, which is a simple method that adds the values of the object in parameter2 to the object in parameter1. For more information, see YUI yahoo.js.

Next we need to consider what functionality is most needed and missing: word capitalization, stripping characters (alpha, numbers, etc…), stripping tags (script, or all html tags), and trimming white spaces. Obviously, you may want a lot more, but this is these are the ones I use most:

Example 1: Extending Strings.prototype

YAHOO.lang.augmentObject(String.prototype, { /** * Capitolize the first letter of every word; ucfirst, ensures that all non-first letters are lower-case * * @method capitalize * @param ucfirst {boolean} OPTIONAL: when truthy, converts non-first letters to lower-case * @return {string} the converted string * @static */ capitalize: function(ucfirst) { var words = this.split(/\b/g), rs = []; Core.batch(words, function(w, i) { if (w.trim()) { rs[i] = w.charAt(0).toUpperCase() + (ucfirst? w.substring(1).toLowerCase(): w.substring(1)); } }); return rs.join(’ ‘); }, /** * Checks if a string contains any of the strings in the arguement set * * @method contains * @param argument {string} as many strings you want to test * @return {boolean} true, if string contains any of the arguements * @static */ contains: function() { var hasValue = false; Core.batch(arguments, function(arg) { hasValue = -1 < str.indexOf(arg); // terminates iteration if this becomes true return hasValue; }); return hasValue; }, /** * Removes the rx pattern from the string * * @method remove * @param rx {regex} a regex to find characters to remove * @public */ remove: function(rx) { return this.replace(rx, ''); }, /** * Remove all non-alpha characters;space ok * * @method stripNonAlpha * @public */ stripNonAlpha: function() { return this.remove(/[^A-Za-z ]+/g); }, /** * Remove all non-alpha-numeric characters; space ok * * @method stripNonAlphaNumeric * @public */ stripNonAlphaNumeric: function() { return this.remove(/[^A-Za-z0-9 ]+/g); }, /** * Removes non-numeric characters, except minus and decimal * * @method stripNonNumeric * @public */ stripNonNumeric: function() { return this.remove(/[^0-9\-\.]/g); }, /** * Remove all characters that are 0-9 * * @method stripNumeric * @public */ stripNumeric: function() { return this.remove(/[0-9]/g); }, /** * HTML script tags from the string * * @method stripScripts * @public */ stripScripts: function() { return this.remove(new RegExp("(?: )((\n|\r|.)*?)(?:<\/script>)", "img")); }, /** * HTML tags from the string * * @method stripTags * @public */ stripTags: function() { return this.remove(/<\/?[^>]+>/gi); }, /** * Replaces the white spaces at the front and end of the string * OPTIMIZED: http://blog.stevenlevithan.com/archives/faster-trim-javascript * * @method trim * @public */ trim: function() { return this.remove(/^\s\s*/).remove(/\s\s*$/); } });

Some of these methods I have put a lot of thought into, such as trim, which I use frequently, so I ensure that I have the most efficient regex. As you can see, many string manipulations can/should be handled by regex, so it is a good idea to understand regex (hopefully you do). Steven’s Blog is a great place for your regex questions, especially when looking for the best way to write an expression.

Most of these are pretty easy to understand, especially if you look at my comments (feel free to leave a comment if you have questions). The best part is, because you have extended “String.prototype”, every String throughout your entire project will be able to use them. Often times, you will find tasks that are specific toward the current project, but maybe not relevant to every project. For example, Mint.com is a financial site where I often need to search for numbers and/or currency, so I have special methods for that project. It is best to keep these in a separate file and bring them into your project as necessary.

posted by Matt Snider at 5:28 pm  
Next Page »

Powered by WordPress