Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

Thursday, January 31, 2008

News - Prototype, IE7 AutoUpdate, HTML 5

Sorry about the lack of article for Tuesday: I am extremely busy at Mint this week and then spent 9 hours at the hospital (without a my laptop power cable), which pretty much killed any time I had to write an article. Instead here is some good news for Front-End Engineers:

Prototype 1.6.0.2

Prototype 1.6.0.2 has been released with several bugs fixes, including support for Opera 9.25 and a promise to support future version of Opera.

IE7 is Now AutoUpdate

Microsoft is intending to do an auto-update of Internet explorer on the 12th February. So awesome… so awesome. Need I say more!?

HTML 5 Drafted

HTML 5 Seems more complicated to me than it it worth, but here it is anyway.

posted by Matt Snider at 11:12 am  

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  

Tuesday, January 22, 2008

News: IE8 and Compatibility

I am going to forgo the normal article this week, because there is so much talk about compatibility mode in IE8. As any web developer knows, IE6 and lower is different from IE7 is different from the W3C standards. The problem is that as IE improved the engineers at Microsoft not only needed to support standards, but also the thousands of people who had already built websites according to the previous, non-standard compliant versions of IE. This continued up through IE6, then when they released IE7, Microsoft took a different route, supporting more standards, but being less backwards compatible with IE6. As a result the web community cried foul, frustrated at having to support yet another browser (and one that is not completely standards compliant at that).

Enter IE8… IE8 is standards compliant, as we all learned when it passed the acid test. However, it is not backwards compliant with IE7 or IE6 and the engineers realized that there did not yet exist a good way to allow backwards compatibility, which they would need to support their, “don’t break the web” mantra. As a result, they have introduced an IE8+ only meta tag (it won’t affect any other browser at all) that allows the web designer to specify which browser their site is compliant to.

Example 1: Meta Tag

<meta http-equiv=”X-UA-Compatible” content=”IE=8″ />

This solution is simple, elegant, and degrades nicely. My hat is off to Microsoft on this one; I believe they are doing this browser right.

Here are some key articles about this topic:
Compatibility and IE8 by Microsoft
The versioning switch is not a browser detect by PPK
Beyond Doctype by A List Apart

posted by Matt Snider at 9:35 am  

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  

Tuesday, January 15, 2008

XJSON Objects Continued

Today we are concluding our discussion of the xJSON object package. You should have read the following two articles and know what Model.js does, and understand the “extend” function with super class inheritance:

Improving Extend With Super
XJSON Objects

Download the most up-to-date xJSON package.

There are 4 JSON models in this package: JsonArray, JsonObject, XJsonArray, and XJsonObject. As a recap, the xJSON Objects are used to improve the performance of large, repeating data sets. For example, if you have the JSON array:

Example 1: JSON Array

[ {id:1, name: ‘name1′}, {id:2, name: ‘name2′}, {id:3, name: ‘name3′}, {id:4, name: ‘name4′}, {id:5, name: ‘name5′}, … ]

Each object in the array contains key/value pairs, with the keys repeated in each object. You can reduce the size of such a message by using a compression technique, known as xJSON:

Example 2: xJSON Array

{ scheme: [’id’, ‘name’], set: [ [1, ‘name1′], [2, ‘name2′], [3, ‘name3′], [4, ‘name4′], [5, ‘name5′], … ] … }

These xJSON Classes should produce similar structures (at least publicly) to their JSON counterparts. This way, you don’t care if you have a JsonArray or XJsonArray, only if you have an array versus an object. For the most part, XJsonObject will only be instantiated from XJsonArray, as you are required to pass in a schema, which requires an extra variable that normal objects will not have. One of the trickiest parts is that a JSON object could contain nested JSON objects/arrays, which we need to detect and instantiate on demand. For this purpose I wrote the Model method “XJson” which detects whether the passed object is a JSON object/array or not and returns the appropriate class, null, or simply the variable itself. Therefore, I can use this class anytime I want JSON object, whether from a new JSON object or when evaluating the values inside of a JSON object.

Example 3: XJson

Core.Model.XJson = function(o) { if (! o) { // test 1 return null; } else if (isType(o, ‘array’) && isType(o[0], ‘object’)) { // test 2 return new Core.Model.JsonArray(o); } else if (isType(o, ‘object’) && ! isType(o, ‘array’)) { // test 3 return (o.scheme)? new Core.Model.XJsonArray(o): new Core.Model.JsonObject(o); } else { // test 4 return o; } };

This Function examines the passed object, returning null if no parameter is passed (ie. there is no data) and the parameter itself when it does not match JSON criteria. The second test checks to see if the parameter is an array and that the first member is an object, which generally means it is a JsonArray (this is a weak assumption, please comment if you have a better idea). The third check tests if the parameter is an object, but not an array primitive. Then if it has a scheme value, the data object is assumed to an XJsonArray, otherwise, it is a JsonObject (the assumption here is that you never use ’scheme’ as a key for a non-xJSON object).

Event though we already discussed JsonObject and JsonArray, I want to revisit them today, as they have changed quite a bit and will form the basis on which we can discuss the xJSON objects.

Example 4: JSON Object

Core.Model.JsonObject = function(json) { this.update(json); }; Core.extend(Core.Model.JsonObject, Core.Model.Model, { /** * The number of values in the json object * * @property length * @type int */ length: 0, /** * Updates the object to use the passed data set * * @method update * @param json {array} jsonobject object * @public */ update: function(json) { // validation if (! isType(json, ‘object’)) {throw(’JsonObject - Invalid data passed into Update’);} this.parent.update.call(this, json); // private variables var data = {}, that = this, n = 0; var fx = function(key, o) { var ckey = key.replace(/^is/, ”).capitalize(); data[key] = Core.Model.XJson(o); that[(isType(o, ‘boolean’)? ‘is’: ‘get’) + ckey] = function() {return data[key];}; that[’set’ + ckey] = function(o) { if ($type(o) !== $type(data[key])) {throw(’JSONObject - invalid object passed into setter for: ‘ + key);} data[key] = o; }; }; // iterate through the JSON object keys for (var key in json) { fx(key, json[key]); n += 1; }; // update public variables this.length = n; } });

The first thing to notice is that this object and all the future object will extend the Model object. They will also have a public value “length”, which for the (x)JsonObject types will be the number of key/value pairs and for (x)JsonArray types will be the length of the JSON array. Each object also has an update method that first validates the data passed to it, then calls the update method of the super class “Model”.

For JsonObject Class we first create a local “data” Object and the Function “fx” to create a closure around the “data” variable. Next we iterate through all the keys in the JSON Object and fill the “data” variable with the evaluated values. Lastly, we create the setter and getter function for each key/value pair, which set or update the value in the “data” variable. The only caveat is that booleans values get an “is” instead of a “get” in front of them. Booleans should always start with question verbs (such as “is”, “has”, etc.). I find that I use “is” 90% of the time, so I just went ahead and now use “is” 100% of the time. You can remove the ternary statement and just preface the methods with “get” if you do not structure your booleans this way. Lastly, when using a setter method, we check to ensure that both the object we are replacing and the object we are passing at least have the same type, which should help reduce errors.

Example 5: JSON Array

Core.Model.JsonArray = function(json) { this.update(json); }; Core.extend(Core.Model.JsonArray, Core.Model.Model, { /** * The number of values in the json array * * @property length * @type int */ length: 0, /** * Execute function ‘fn’ on all elements in collection * * @method batch * @param fn {function} the function to execute * @public */ batch: function(fn) { Core.batch(this.data, function(o, i, scope) { fn(scope.get(i), i); }, this); }, /** * Retrieve the element at ‘i’ from the collection; lazy definition to allow for converting objects on demand * * @method get * @param i {int} index in the data * @public */ get: function(i) { var that = this, data = []; this.get = function(i) { // commented because this is very strict and don’t always want to use, sometimes returning undefined is ok //if (! that.data[i]) {throw(’JsonArray - Invalid index passed into Get: ‘ + i);} if (! data[i] && that.data[i]) { data[i] = Core.Model.XJson(that.data[i]); } return data[i]; }; return this.get(i); }, /** * Inserts another value into the data structure * * @method push * @param o {array} new row of data * @public */ push: function(o) { if (! o) {throw(’JsonArray - Invalid Object passed into Push’);} this.data.push(o); this.length = this.data.length; }, /** * Updates the object to use the passed data set * * @method update * @param json {array} jsonarray object * @public */ update: function(json) { // validation if (! isType(json, ‘array’)) {throw(’JSONArray - Invalid JSON Array Object passed into Update’);} this.parent.update.call(this, json); // update public variables this.length = json.length; } });

First, notice that this Class has a similar structure to that of JsonObject (all of the JSON object types do). Now, a JSON array should just be a collection of JSON objects, and we have made that assumption here. The get method, uses lazy initialization to create an internal “data” variable, which will be updated each time an index “i” is requested. This prevents us from having to initialize each JsonObject until it is requested, thereby making this an inexpensive transformation. If you instantiated each JsonObject in the update method, you run the risk of monopolizing the client’s CPU when working with medium to large JSON arrays. The “batch” function is a handle shortcut that calls “get” on all indexes, then executes the passed Function on the result. Lastly, I added “push” for convenience because I occasionally find myself adding elements to the object. A “pop” or “remove” method is probably useful, but I have not had a need for one yet.

Example 6: xJSON Object

Core.Model.XJsonObject = function(scheme, json) { if (! isType(scheme, ‘array’)) {throw(’XJsonObject - Invalid scheme passed into Constructor’);} this.scheme = scheme; this.update(json); }; Core.extend(Core.Model.XJsonObject, Core.Model.Model, { /** * The number of values in the xjson object * * @property length * @type int */ length: 0, /** * The object schema * * @property scheme * @type array */ scheme: [], /** * Updates the object to use the passed data set * * @method update * @param json {object} XJsonObject object * @public */ update: function(json) { // validation if (! isType(json, ‘array’)) {throw(’XJsonObject - Invalid data passed into Update’);} if (json.length !== this.scheme.length) {throw(’XJsonObject - Invalid data (does not match scheme) passed into Update’);} this.parent.update.call(this, json); // private variables var data = this.data, that = this; // update public variables this.length = data.length; Core.batch(this.scheme, function(key, i, json) { var o = json[i], ckey = key.replace(/^is/, ”).capitalize(); data[i] = Core.Model.XJson(o); that[(isType(o, ‘boolean’)? ‘is’: ‘get’) + ckey] = function() {return data[i];}; that[’set’ + ckey] = function(o) { if ($type(o) !== $type(data[key])) {throw(’XJSONObject - invalid object passed into setter for: ‘ + key);} data[i] = o; }; }, data); } });

XJsonObject is actually very similar to JsonObject, except instead of having to iterate on the key/value pairs of a JavaScript Object, you just iterate through an array of schema and a data array. The scheme is stored in a global array and except in rare circumstances when you have a constant schema that you use in several places, this method will only be called by XJsonArray.

Example 7: xJSON Array

Core.Model.XJsonArray = function(json) { this.update(json); }; Core.extend(Core.Model.XJsonArray, Core.Model.Model, { /** * The number of values in the xjson array * * @property length * @type int */ length: 0, /** * The object schema * * @property scheme * @type array */ scheme: [], /** * Execute function ‘fn’ on all elements in collection * * @method batch * @param fn {function} the function to execute * @public */ batch: function(fn) { Core.batch(this.data, function(o, i, scope) { fn(scope.get(i), i); }, this); }, /** * Retrieve the element at ‘i’ from the collection; lazy definition to allow for converting objects on demand * * @method get * @param i {int} index in the data * @public */ get: function(i) { var that = this, data = []; // internally redeclaring to scope the data structure, which will contain already converted objects this.get = function(i) { // commented because this is very strict and don’t always want to use, sometimes returning undefined is ok //if (! that.data[i]) {throw(’XJsonArray - Invalid index passed into Get: ‘ + i);} if (! data[i]) { var o = that.data[i]; // if array, then it is an XJsonObject if (isType(o, ‘array’) && o.length === that.scheme.length) { data[i] = new Core.Model.XJsonObject(that.scheme, o); } // otherwise, use generic method else { data[i] = Core.Model.XJson(o); } } return data[i]; }; return this.get(i); }, /** * Inserts another value into the data structure * * @method push * @param o {array} new row of data * @public */ push: function(o) { if (! (isType(o, ‘array’) && o.length === this.scheme.length)) {throw(’XJsonArray - Invalid data passed into Push’);} this.data.push(o); this.length = this.data.length; }, /** * Updates the object to use the passed data set * * @method update * @param json {object} xjsonarray object * @public */ update: function(json) { // validation if (! (json && isType(json, ‘object’) && isType(json.scheme, ‘array’) && isType(json.set, ‘array’))) {throw(’XJsonArray - Invalid data passed into Update’);} this.parent.update.call(this, json.set); // update public variables this.length = json.length; this.scheme = json.scheme; } });

This Class is very similar to JsonArray, but with a public “scheme” array as with XJsonObject. When the “update” Function calls the super class “update”, it passes in the “set” parameter, so the public “data” variable is an array like that of JsonArray. The “get” Function is only different from JsonArray, in that it first looks to see if the value at “i” is an xJSON object before it defers to other objects.

Obviously, this these Classes are fairly complex in their implementation, but they are easy to use. Simply, call “Mint.Model.XJson(yourJsonObject)” and the results will be evaluated and easy to use.

Download “xJson.js”

See My Simple Test Page

posted by Matt Snider at 10:15 pm  

Saturday, January 12, 2008

xJson Objects

Alright everyone, I have finished the xJSON object suite, or at least a decent first pass. This was a bigger project than I expected and I apologize for dragging it out over the past couple weeks. It seemed like every time I was about to finish, I would uncover a corner case that would break an entire second of the project. Anyway, I was away over the passed weekend and had to do a lot work in the evening last week, and missed your Tuesday post as well. I hope to be better about getting two articles written a week. I am also open to guest posts, if you are interest just leave a comment on any article.

Anyway, I have written the following objects: JsonObject, JsonArray, XJsonObject, and XJsonArray. Here they are:

/** * The JsonObject class manages a JSON object by creating getter and setters * * @namespace Core.Model * @class JsonObject * @dependencies library */ Core.Model.JsonObject = function(json) { this.update(json); }; Core.extend(Core.Model.JsonObject, Core.Model.Model, { /** * The number of values in the json object * * @property length * @type int */ length: 0, /** * Updates the object to use the passed data set * * @method update * @param json {array} jsonobject object * @public */ update: function(json) { // validation if (! isType(json, ‘object’)) {throw(’JsonObject - Invalid data passed into Update’);} this.parent.update.call(this, json); // private variables var data = {}, that = this, n = 0; var fx = function(key, o) { var ckey = capitalize(key); data[key] = Core.Model.XJson(o); that[(isType(o, ‘boolean’) && -1 !== key.indexOf(’is’))? key: ‘get’ + ckey] = function() {return data[key];}; that[’set’ + ckey] = function(o) { if ($type(o) !== $type(data[key])) {throw(’JSONObject - invalid object passed into setter for: ‘ + key);} data[key] = o; }; }; // iterate through the JSON object keys for (var key in json) { fx(key, json[key]); n += 1; }; // update public variables this.length = n; } }); /** * The JsonArray class manages … * * @namespace Core.Model * @class JsonArray * @dependencies library */ Core.Model.JsonArray = function(json) { this.update(json); }; Core.extend(Core.Model.JsonArray, Core.Model.Model, { /** * Boolean used to inform the ‘get’ method to clean the cache * * @property clean * @type boolean */ clean: false, /** * The number of values in the json array * * @property length * @type int */ length: 0, /** * Execute function ‘fn’ on all elements in collection * * @method batch * @param fn {function} the function to execute * @public */ batch: function(fn) { Core.batch(this.data, function(o, i, scope) { fn(scope.get(i), i); }, this); }, /** * Retrieve the element at ‘i’ from the collection; lazy definition to allow for converting objects on demand * * @method get * @param i {int} index in the data * @public */ get: function(i) { var that = this, data = []; this.get = function(i) { // commented because this is very strict and don’t always want to use, sometimes returning undefined is ok //if (! that.data[i]) {throw(’JsonArray - Invalid index passed into Get: ‘ + i);} // purges cached data on an update if (that.clean) { data = []; that.clean = false; } if (! data[i] && that.data[i]) { data[i] = Core.Model.XJson(that.data[i]); } return data[i]; }; return this.get(i); }, /** * Inserts another value into the data structure * * @method push * @param o {array} new row of data * @public */ push: function(o) { if (! o) {throw(’JsonArray - Invalid Object passed into Push’);} this.data.push(o); this.length = this.data.length; }, /** * Updates the object to use the passed data set * * @method update * @param json {array} jsonarray object * @public */ update: function(json) { // validation if (! isType(json, ‘array’)) {throw(’JSONArray - Invalid JSON Array Object passed into Update’);} this.parent.update.call(this, json); this.clean = true; // update public variables this.length = json.length; } }); /** * The XJsonObject class manages … * * @namespace Core.Model * @class XJsonObject * @dependencies library */ Core.Model.XJsonObject = function(scheme, json) { if (! isType(scheme, ‘array’)) {throw(’XJsonObject - Invalid scheme passed into Constructor’);} this.scheme = scheme; this.update(json); }; Core.extend(Core.Model.XJsonObject, Core.Model.Model, { /** * The number of values in the xjson object * * @property length * @type int */ length: 0, /** * The object schema * * @property scheme * @type array */ scheme: [], /** * Updates the object to use the passed data set * * @method update * @param json {object} XJsonObject object * @public */ update: function(json) { // validation if (! isType(json, ‘array’)) {throw(’XJsonObject - Invalid data passed into Update’);} if (json.length !== this.scheme.length) {throw(’XJsonObject - Invalid data (does not match scheme) passed into Update’);} this.parent.update.call(this, json); // private variables var data = this.data, that = this; // update public variables this.length = data.length; Core.batch(this.scheme, function(key, i, json) { var o = json[i], ckey = capitalize(key); data[i] = Core.Model.XJson(o); that[(isType(o, ‘boolean’) && -1 !== key.indexOf(’is’))? key: ‘get’ + ckey] = function() {return data[i];}; that[’set’ + ckey] = function(o) { if ($type(o) !== $type(data[key])) {throw(’XJSONObject - invalid object passed into setter for: ‘ + key);} data[i] = o; }; }, data); } }); /** * The XJsonArray class manages … * * @namespace Core.Model * @class XJsonArray * @dependencies library */ Core.Model.XJsonArray = function(json) { this.update(json); }; Core.extend(Core.Model.XJsonArray, Core.Model.Model, { /** * Boolean used to inform the ‘get’ method to clean the cache * * @property clean * @type boolean */ clean: false, /** * The number of values in the xjson array * * @property length * @type int */ length: 0, /** * The object schema * * @property scheme * @type array */ scheme: [], /** * Execute function ‘fn’ on all elements in collection * * @method batch * @param fn {function} the function to execute * @public */ batch: function(fn) { Core.batch(this.data, function(o, i, scope) { fn(scope.get(i), i); }, this); }, /** * Retrieve the element at ‘i’ from the collection; lazy definition to allow for converting objects on demand * * @method get * @param i {int} index in the data * @public */ get: function(i) { var that = this, data = []; // internally redeclaring to scope the data structure, which will contain already converted objects this.get = function(i) { // commented because this is very strict and don’t always want to use, sometimes returning undefined is ok //if (! that.data[i]) {throw(’XJsonArray - Invalid index passed into Get: ‘ + i);} // purges cached data on an update if (that.clean) { data = []; that.clean = false; } if (! data[i]) { var o = that.data[i]; // if array, then it is an XJsonObject if (isType(o, ‘array’)) { data[i] = new Core.Model.XJsonObject(that.scheme, o); } // otherwise, use generic method else { data[i] = Core.Model.XJson(o); } } return data[i]; }; return this.get(i); }, /** * Inserts another value into the data structure * * @method push * @param o {array} new row of data * @public */ push: function(o) { if (! (isType(o, ‘array’) && o.length === this.scheme.length)) {throw(’XJsonArray - Invalid data passed into Push’);} this.data.push(o); this.length = this.data.length; }, /** * Updates the object to use the passed data set * * @method update * @param json {object} xjsonarray object * @public */ update: function(json) { // validation if (! (json && isType(json, ‘object’) && isType(json.scheme, ‘array’) && isType(json.set, ‘array’))) {throw(’XJsonArray - Invalid data passed into Update’);} this.parent.update.call(this, json.set); this.clean = true; // update public variables this.length = json.length; this.scheme = json.scheme; } }); /** * The XJson class manages … * * @namespace Core.Model * @class XJson * @dependencies library */ Core.Model.XJson = function(o) { if (! o) { return null; } else if (isType(o, ‘array’) && (isType(o[0], ‘array’) || ! o.length)) { return new Core.Model.JsonArray(o); } else if (isType(o, ‘object’) && ! isType(o, ‘array’)) { return (o.scheme)? new Core.Model.XJsonArray(o): new Core.Model.JsonObject(o); } else { return o; } };

This is a lot of code to discuss, which I do not have time for today (Tuesday’s article will have a detailed explanation). I have introduced a String manipulation method ‘capitalize’, which converts strings like ‘mattsnider’ to ‘Mattsnider’. This way all getter and setter methods have the appropriate capitalization. This method is from a small package of string functions, which we will discuss after these JSON objects. For now, let me know your thoughts about these objects and take a look at the test page.

posted by Matt Snider at 12:01 pm  

Saturday, January 5, 2008

Improving Extend With Super

Alright, it looks like I will need to eat my words about not needing super class support in my extend Function, see Object Extension. As many of you know, I have been working on Objects to represent XJson in JavaScript. One of the changes from my original JSONObject Model is that I now use “Core.extend” to extend a core Model object. This object has an onChange event and an update function. Here is what I have so far:

Example 1: Model.js

Core.Model.Model = function(o) { this.update(o); }; Core.Model.Model.prototype = { addChangeListener: function(fx) { var evt = new YAHOO.util.CustomEvent(’model’ + Math.random(), this, false, YAHOO.util.CustomEvent.FLAT), that = this; this.addChangeListener = function(fx) {evt.subscribe(fx, that);}; this.addChangeListener(fx); }, update: function(o) { if (! o) {throw(’Model - Invalid object passed into Update’);} this.data = o; } };

Model does little right now, but what I realized is that any Object that extends from it and overrides the update method, should first call the update method of Model. This way the passed Object is always evaluated and “this.data” is always created. In the future, I may also find other important values to attach. In order to make this call, I needed access to the super class of an extending Object.

As I used YUI as a model for my extend function, it is also a good model for how to do super classes. OOP in JS, Part 2 is a useful article as well. Here is a revamped version of extend to support super classes:

Example 2: Superclasses

var extend = function(subc, superc, overrides) { if (! superc || ! subc) { throw new Error(’Core.extend failed, please check that all dependencies are included.’); } var F = function() {}; F.prototype = superc.prototype; subc.prototype = new F(); subc.prototype.constructor = subc; subc.prototype.parent = superc.prototype; if (overrides) { for (var i in overrides) { subc.prototype[i] = overrides[i]; } } };

I have taken a very similar approach to YUI, however, there are two notable changes: 1) I use ‘parent’ as the super class reference, instead of ’superclass’; as a DOMScripter parent is a familiar naming convention and it feels more intuitive, 2) when the super class is ‘Object’ nothing is attached to the prototype of ‘Object’; I hate attaching anything to the prototype of Objects and Arrays as it messes up ‘for … in’ statements.

So, lastly, here is an example of how you call the super class from the child class:

Example 3: Calling the Super Class

Core.Model.XJsonArray = function(data) { this.update(data); }; Core.extend(Core.Model.XJsonArray, Core.Model.Model, { length: 0, … /* More to come in my Tuesday Article */ /** * Updates the object to use the passed data set * * @method update * @param data {object} xjsonarray object * @public */ update: function(data) { if (! (data && isType(data, ‘object’) && isType(data.scheme, ‘array’) && isType(data.set, ‘array’))) {throw(’XJsonArray - Invalid data passed into Update’);} this.parent.update.call(this, data.set); this.length = this.data.length; this.scheme = data.scheme; } });
posted by Matt Snider at 1:35 am  

Powered by WordPress