Matt Snider JavaScript Resource

Understanding JavaScript and Frameworks

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  

Friday, December 7, 2007

JsonObject Model Prototype

I have been doing a lot of server-side work in PHP and JAVA this past month. One of the design practices used by server-side webapp languages is the MVC (model-view-controller) framework. An MVC makes writing extensible and scalable code, much easier. In JavaScript I have seen a couple of attempts at a similar MVC framework, but most of the time it is overkill or poorly implemented. Shutterfly.com on the other-hand, has a lot of JavaScript and a fairly solid MVC implementation.

I have been toying with JavaScript MVC concepts all week and want to share what I developed to improve working with JSON objects. Keep in mind this is a work in progress and I am open to any suggestions:

Example 1: Sample Code

/** * The JsonObject class manages … * * @namespace Core.Model * @class JsonObject * @dependencies core */ Core.Model.JsonObject = function(data) { this.update(data); }; Core.Model.JsonObject.prototype = { JKEYS: [], MODEL: ‘JsonObject’, update: function(data) { var obj = {}, that = this, i = 0; // iterate through the JSON object keys for (var key in data) { var o = data[key]; if (! isType(o, ‘function’)) { // this key is not in the model if (-1 < that.JKEYS.indexOf(key)) { throw('Invalid key (' + key + ') passed into ' + that.MODEL); } obj[key] = o; that[isType(o, 'boolean')? key: 'get' + ckey] = function() {return obj[key];}; that['set' + ckey] = function(o) {return obj[key] = o;}; i += 1; } }; // the keys in the object do not match the keys in the model if (i != JKEYS.length) { throw('Invalid number of keys passed into ' + that.MODEL); } } });

This method takes a JSON object and applies getter/setter methods to that object. Each time update is called, it validates against JSKEYS, which should be an array of the expected keys. We also remove any functions that might have been attached to the ‘data’ object. You may also note that I have a special check for boolean values that does not attach the ‘get’ preface to the getter method. This is because I always preface boolean values with ‘is’ or ‘has’ (or something like that), and I want that preserved in the Function attached to the JsonObject.

Here is an example of how you might use it:

Example 2: Using JsonObject

// a JSON object, most likely returned from the server, representing a car var carJson = { make: ‘ford’, model: ‘contour’, year: ‘1996′, price: ‘$2,000′, isNew: false }; // use the extend (Object Extension) method to create a CarObject, with the appropriate JKEYS set Core.extend(Core.Model.CarObject, Core.Model.JsonObject, { JKEYS: [’make’, ‘model’, ‘year’ , ‘price’], MODEL: ‘CarObject’ }); var car = new Core.Model.CarObject(carJson);

Example 2 will create a CarObject (extended JsonObject), with the following members:

Example 3: CarObject Structure

CarObject = { // constants defined by the prototype object JKEYS: [’make’, ‘model’, ‘year’ , ‘price’], MODEL: ‘CarObject’, getMake(), setMake(), getModel(), setModel(), getYear(), setYear(), getPrice(), setPrice(), isNew(), setIsNew() }

I find it much easier to work with the JsonObjects once the getters and setters have been created, because you have defined what should be there. Plus, with the validation you can be sure that you received the correct JSON Object. Some possible improvements would be to support nested JsonObjects and JsonArrays, and setter type validation (although, this might be overkill).

There are really only 3 types of models that you might need to use in JavaScript: JSON Objects/Arrays, Query strings, and XML. Creating the Models is a good first step in building an MVC as you will need to pass these between the controller and the view, to update the view and send messages to the server. Next week, I will look into these other model types and improve this one a bit.

posted by Matt Snider at 3:35 pm  

Powered by WordPress