Getting the Most Out of jQuery Widgets

Post on 19-May-2015

2.094 views 0 download

Tags:

description

Richard Lindsey's presentation from the 2013 jQuery Conference in Austin, Tx.

Transcript of Getting the Most Out of jQuery Widgets

Richard Lindsey @Velveeta http://conqueringtheclient.com/PLATFORM FEE ARCHITECT | THE ADVISORY BOARD COMPANY

jQuery Widgets

GETTING THE MOST OUT OF

Let’s say we’re making Widgets…

Richard Lindsey @Velveeta http://conqueringtheclient.com/

What’s a Widget?

Richard Lindsey @Velveeta http://conqueringtheclient.com/

ELEMENTS / COMPOUNDS /CELLS / ORGANISMS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Thinksmall.Thinkmodular.

Communicate through

events.KEEP COMPONENTS DECOUPLED / MAKE THEM SUBSCRIBE AND RESPOND

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Communicate through

events.KEEP COMPONENTS DECOUPLED / MAKE THEM SUBSCRIBE AND RESPOND

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Observe and

mediate.BUNDLE SMALLER MODULES / PROVIDE PUBLIC API / DIRECT REFERENCES SHOULD ONLY GO DOWNWARDS / EACH LAYER CONSUMES LOWER-LEVEL EVENTS & PUBLISHES UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Observe and

mediate.BUNDLE SMALLER MODULES / PROVIDE PUBLIC API / DIRECT REFERENCES SHOULD ONLY GO DOWNWARDS / EACH LAYER CONSUMES LOWER-LEVEL EVENTS & PUBLISHES UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Observe and

mediate.BUNDLE SMALLER MODULES / PROVIDE PUBLIC API / DIRECT REFERENCES SHOULD ONLY GO DOWNWARDS / EACH LAYER CONSUMES LOWER-LEVEL EVENTS & PUBLISHES UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Observe and

mediate.BUNDLE SMALLER MODULES / PROVIDE PUBLIC API / DIRECT REFERENCES SHOULD ONLY GO DOWNWARDS / EACH LAYER CONSUMES LOWER-LEVEL EVENTS & PUBLISHES UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

$.widget(‘abc.autocomplete’, {_create: function () {

this._widgets = {dataloader: {loader:{}},optionlist: {results:{}},input: {search:{}}

};this._createWidgets();this._routeTraffic();

},_routeTraffic: function () {

this._on(this.element, { autocompletesuccess: this._showOptionList });this._on(this._widgets.search, { inputkeydown: _.debounce(this._updateDataloaderSearchParam, 100) });this._on(this._widgets.results, { optionlistselected: this._updateInput });

},_updateDataloaderSearchParam: function (e, search) {

var deferred = this._widgets.loader .dataloader(‘updateParams’, ‘search’, search) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},_showOptionList: function () {

this._widgets.results.optionlist(‘show);this._trigger(‘showresults’);

},_updateInput: function (e, value) {

this._widgets.search.input(‘setValue’, value);this._trigger(‘change’, value);

},setData: function (data) {

var deferred = this._widgets.loader .dataloader(‘setData’, data) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},setValue: function (value) {

this._updateInput(null, value);}

});

$(function () {$(‘abc-autocomplete’).autocomplete();

});

$.widget(‘abc.autocomplete’, {_create: function () {

this._widgets = {dataloader: {loader:{}},optionlist: {ddl:{}},input: {search:{}}

};this._createWidgets();this._routeTraffic();

},_routeTraffic: function () {

this._on(this.element, { autocompletesuccess: this._showOptionList });this._on(this._widgets.search, { inputkeydown: _.debounce(this._updateDataloaderSearchParam, 100) });this._on(this._widgets.results, { optionlistselected: this._updateInput });

},_updateDataloaderSearchParam: function (e, search) {

var deferred = this._widgets.loader .dataloader(‘updateParams’, ‘search’, search) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},_showOptionList: function () {

this._widgets.results.optionlist(‘show);this._trigger(‘showresults’);

},_updateInput: function (e, value) {

this._widgets.search.input(‘setValue’, value);this._trigger(‘change’, value);

},setData: function (data) {

var deferred = this._widgets.loader .dataloader(‘setData’, data) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},setValue: function (value) {

this._updateInput(null, value);}

});

$(function () {$(‘abc-autocomplete’).autocomplete();

});

$.widget(‘abc.autocomplete’, {_create: function () {

this._widgets = {dataloader: {loader:{}},optionlist: {ddl:{}},input: {search:{}}

};this._createWidgets();this._routeTraffic();

},_routeTraffic: function () {

this._on(this.element, { autocompletesuccess: this._showOptionList });this._on(this._widgets.search, { inputkeydown: _.debounce(this._updateDataloaderSearchParam, 100) });this._on(this._widgets.results, { optionlistselected: this._updateInput });

},_updateDataloaderSearchParam: function (e, search) {

var deferred = this._widgets.loader .dataloader(‘updateParams’, ‘search’, search) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},_showOptionList: function () {

this._widgets.results.optionlist(‘show);this._trigger(‘showresults’);

},_updateInput: function (e, value) {

this._widgets.search.input(‘setValue’, value);this._trigger(‘change’, value);

},setData: function (data) {

var deferred = this._widgets.loader .dataloader(‘setData’, data) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},setValue: function (value) {

this._updateInput(null, value);}

});

$(function () {$(‘abc-autocomplete’).autocomplete();

});

$.widget(‘abc.autocomplete’, {_create: function () {

this._widgets = {dataloader: {loader:{}},optionlist: {ddl:{}},input: {search:{}}

};this._createWidgets();this._routeTraffic();

},_routeTraffic: function () {

this._widgets.loader.on(‘dataloadersuccess’, this._updateDropdownlist);this._widgets.ddl.on(‘dropdownlistselected’, this._updateInput);this._widgets.search.on(‘inputkeydown’, _.debounce(this._updateDataloaderSearchParam, 300));

},_updateDataloaderSearchParam: function (e, search) {

var deferred = this._widgets.loader .dataloader(‘updateParams’, ‘search’, search) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},_showOptionList: function () {

this._widgets.results.optionlist(‘show);this._trigger(‘showresults’);

},_updateInput: function (e, value) {

this._widgets.search.input(‘setValue’, value);this._trigger(‘change’, value);

},setData: function (data) {

var deferred = this._widgets.loader .dataloader(‘setData’, data) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},setValue: function (value) {

this._updateInput(null, value);}

});

$(function () {$(‘abc-autocomplete’).autocomplete();

});

$.widget(‘abc.autocomplete’, {_create: function () {

this._widgets = {dataloader: {loader:{}},optionlist: {ddl:{}},input: {search:{}}

};this._createWidgets();this._routeTraffic();

},_routeTraffic: function () {

this._widgets.loader.on(‘dataloadersuccess’, this._updateDropdownlist);this._widgets.ddl.on(‘dropdownlistselected’, this._updateInput);this._widgets.search.on(‘inputkeydown’, _.debounce(this._updateDataloaderSearchParam, 300));

},_updateDataloaderSearchParam: function (e, search) {

var deferred = this._widgets.loader .dataloader(‘updateParams’, ‘search’, search) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},_showOptionList: function () {

this._widgets.results.optionlist(‘show);this._trigger(‘showresults’);

},_updateInput: function (e, value) {

this._widgets.search.input(‘setValue’, value);this._trigger(‘change’, value);

},setData: function (data) {

var deferred = this._widgets.loader .dataloader(‘setData’, data) .dataloader(‘fetch’);

this._trigger(‘fetch’, deferred);},setValue: function (value) {

this._updateInput(null, value);}

});

$(function () {$(‘abc-autocomplete’).autocomplete();

});

Richard Lindsey @Velveeta http://conqueringtheclient.com/

BAD IDEAAHEAD

Decorate ALL thefunction

s!

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Richard Lindsey @Velveeta http://conqueringtheclient.com/

MODIFY THE FACTORY FUNCTION IF YOU NEED TO

Decorate ALL thefunction

s!

var widgetFactory = $.widget;

$.widget = function (name, base, prototype) {var targetPrototype = prototype || base;

$.each(targetPrototype, function (key, callback) {if (typeof callback === ‘function’) {

targetPrototype[key] = function () {if (someConditionPasses) {fireSomeFunction();}

var result = callback.apply(this, arguments);

if (someOtherConditionPasses) {fireSomeOtherFunction();}

return result;};

}});return widgetFactory.apply(this, arguments);

};

// The widget factory function itself has some function members itself,// like $.widget.bridge and $.widget.extend. Don’t forget to copy those// items over from the original factory to our new implementation!$.each(widgetFactory, function (key, value) {

$.widget[key] = value;});

var widgetFactory = $.widget;

$.widget = function (name, base, prototype) {var targetPrototype = prototype || base;

$.each(targetPrototype, function (key, callback) {if (typeof callback === ‘function’) {

targetPrototype[key] = function () {if (someConditionPasses) {fireSomeFunction();}

var result = callback.apply(this, arguments);

if (someOtherConditionPasses) {fireSomeOtherFunction();}

return result;};

}});return widgetFactory.apply(this, arguments);

};

// The widget factory function itself has some function members itself,// like $.widget.bridge and $.widget.extend. Don’t forget to copy those// items over from the original factory to our new implementation!$.each(widgetFactory, function (key, value) {

$.widget[key] = value;});

var widgetFactory = $.widget;

$.widget = function (name, base, prototype) {var targetPrototype = prototype || base;

$.each(targetPrototype, function (key, callback) {if (typeof callback === ‘function’) {

targetPrototype[key] = function () {if (someConditionPasses) {fireSomeFunction();}

var result = callback.apply(this, arguments);

if (someOtherConditionPasses) {fireSomeOtherFunction();}

return result;};

}});return widgetFactory.apply(this, arguments);

};

// The widget factory function itself has some function members itself,// like $.widget.bridge and $.widget.extend. Don’t forget to copy those// items over from the original factory to our new implementation!$.each(widgetFactory, function (key, value) {

$.widget[key] = value;});

var widgetFactory = $.widget;

$.widget = function (name, base, prototype) {var targetPrototype = prototype || base;

$.each(targetPrototype, function (key, callback) {if (typeof callback === ‘function’) {

targetPrototype[key] = function () {if (someConditionPasses) {fireSomeFunction();}

var result = callback.apply(this, arguments);

if (someOtherConditionPasses) {fireSomeOtherFunction();}

return result;};

}});return widgetFactory.apply(this, arguments);

};

// The widget factory function itself has some function members itself,// like $.widget.bridge and $.widget.extend. Don’t forget to copy those// items over from the original factory to our new implementation!$.each(widgetFactory, function (key, value) {

$.widget[key] = value;});

var widgetFactory = $.widget;

$.widget = function (name, base, prototype) {var targetPrototype = prototype || base;

$.each(targetPrototype, function (key, callback) {if (typeof callback === ‘function’) {

targetPrototype[key] = function () {if (someConditionPasses) {fireSomeFunction();}

var result = callback.apply(this, arguments);

if (someOtherConditionPasses) {fireSomeOtherFunction();}

return result;};

}});return widgetFactory.apply(this, arguments);

};

// The widget factory function itself has some function members itself,// like $.widget.bridge and $.widget.extend. Don’t forget to copy those// items over from the original factory to our new implementation!$.each(widgetFactory, function (key, value) {

$.widget[key] = value;});

Richard Lindsey @Velveeta http://conqueringtheclient.com/

ALWAYS TRY TO USE PUBLIC API FOR FORWARD COMPATIBILITY

Decorate ALL thefunction

s!

Richard Lindsey @Velveeta http://conqueringtheclient.com/

WHO CARES ABOUT INTERNAL IMPLEMENTATIONS?

Feel free to

mix it up.

Richard Lindsey @Velveeta http://conqueringtheclient.com/

OVERRIDE FUNCTIONALITY IN ONE OF TWO WAYS:

Feel free to

mix it up.

$.widget Factory

Widget Options

• Overrides prototype, affects all instances

• Maintains pointer to overridden function via _super and _superApply

• Overrides instance-level functionality only

• Provides easy access to consumers to override functionality

$.widget(‘abc.dataloader’, {options: {url: null,success: function (results) {this.element.html(JSON.stringify(results));},// etc},fetch: function () {this.element.addClass(‘loading’);return this._load().done($.proxy(function (results) {this.options.success.call(this, results);}, this).always($.proxy(function () {this.element.removeClass(‘loading’);}, this));},_load: function () {return $.ajax(this.options);}

});$.widget(‘abc.dataloader’, abc.dataloader, {

_load: function () {var deferred = $.Deferred();this.element.data(‘backboneCollection’).fetch({reset: true,success: function (collection) {deferred.resolve(collection.toJSON());},error: function (collection, response) {deferred.reject(response);}});return deferred.promise();}

});

var myTemplate = Handlebars.compile($(‘#myTemplate’).html());$(‘#myDiv’).dataloader({

success: function (results) {this.element.html(myTemplate(results));}

});

$.widget(‘abc.dataloader’, {options: {url: null,success: function (results) {this.element.html(JSON.stringify(results));},// etc},fetch: function () {this.element.addClass(‘loading’);return this._load().done($.proxy(function (results) {this.options.success.call(this, results);}, this).always($.proxy(function () {this.element.removeClass(‘loading’);}, this));},_load: function () {return $.ajax(this.options);}

});$.widget(‘abc.dataloader’, abc.dataloader, {

_load: function () {var deferred = $.Deferred();this.element.data(‘backboneCollection’).fetch({reset: true,success: function (collection) {deferred.resolve(collection.toJSON());},error: function (collection, response) {deferred.reject(response);}});return deferred.promise();}

});

var myTemplate = Handlebars.compile($(‘#myTemplate’).html());$(‘#myDiv’).dataloader({

success: function (results) {this.element.html(myTemplate(results));}

});

$.widget(‘abc.dataloader’, {options: {

url: null,success: function (results) {this.element.html(JSON.stringify(results));},// etc

},fetch: function () {

this.element.addClass(‘loading’);return this._load().done($.proxy(function (results) {this.options.success.call(this, results);}, this).always($.proxy(function () {this.element.removeClass(‘loading’);}, this));

},_load: function () {

return $.ajax(this.options);}

});$.widget(‘abc.dataloader’, abc.dataloader, {

_load: function () {var deferred = $.Deferred();this.element.data(‘backboneCollection’).fetch({reset: true,success: function (collection) {deferred.resolve(collection.toJSON());},error: function (collection, response) {deferred.reject(response);}});return deferred.promise();

}});

var myTemplate = Handlebars.compile($(‘#myTemplate’).html());$(‘#myDiv’).dataloader({

success: function (results) {this.element.html(myTemplate(results));

}});

$.widget(‘abc.dataloader’, {options: {url: null,success: function (results) {this.element.html(JSON.stringify(results));},// etc},fetch: function () {this.element.addClass(‘loading’);return this._load().done($.proxy(function (results) {this.options.success.call(this, results);}, this).always($.proxy(function () {this.element.removeClass(‘loading’);}, this));},_load: function () {return $.ajax(this.options);}

});$.widget(‘abc.dataloader’, abc.dataloader, {

_load: function () {var deferred = $.Deferred();this.element.data(‘backboneCollection’).fetch({reset: true,success: function (collection) {deferred.resolve(collection.toJSON());},error: function (collection, response) {deferred.reject(response);}});return deferred.promise();}

});

var myTemplate = Handlebars.compile($(‘#myTemplate’).html());$(‘#myDiv’).dataloader({

success: function (results) {this.element.html(myTemplate(results));}

});

Make it

testable!

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Make it

testable!DOES IT PERFORM A LOGICAL OPERATION OR CALCULATION? / IS IT PART OF THE WIDGET’S PUBLIC-FACING API?

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Make it

testable!DOES IT PERFORM A LOGICAL OPERATION OR CALCULATION? / IS IT PART OF THE WIDGET’S PUBLIC-FACING API?

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Richard Lindsey @Velveeta http://conqueringtheclient.com/

PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY

expose it!

Richard Lindsey @Velveeta http://conqueringtheclient.com/

PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY

expose it!

Richard Lindsey @Velveeta http://conqueringtheclient.com/

PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY

expose it!

ABC = {};(function ($) {

ABC.Prototypes = ABC.Prototypes || {};ABC.Prototypes.demo = {

_create: function () {if (this. _getInstanceCount() === 1) {this._attachListeners();}},_getInstanceCount: function () {return $(‘:abc-demo’).length;},_attachListeners: function () {$(‘body’).on(‘click.demo’, ‘:abc-demo’, $.proxy(this._clickHandler, this));},_clickHandler: function () {console.log(this._getInstanceCount() + ‘ demo widgets instantiated!’);},destroy: function () {if (this._getInstanceCount() === 1) {$(‘body’).off(‘.demo’);this._super();}}

};$(function () {

$(‘.demo’).demo();});

}(jQuery));(function ($) {

$.each(ABC.Prototypes, function (widgetName, widgetPrototype) {$.widget(‘abc.’ + widgetName, widgetPrototype);

});}(jQuery));

ABC = {};(function ($) {

ABC.Prototypes = ABC.Prototypes || {};ABC.Prototypes.demo = {

_create: function () {if (this. _getInstanceCount() === 1) {this._attachListeners();}},_getInstanceCount: function () {return $(‘:abc-demo’).length;},_attachListeners: function () {$(‘body’).on(‘click.demo’, ‘:abc-demo’, $.proxy(this._clickHandler, this));},_clickHandler: function () {console.log(this._getInstanceCount() + ‘ demo widgets instantiated!’);},destroy: function () {if (this._getInstanceCount() === 1) {$(‘body’).off(‘.demo’);this._super();}}

};$(function () {

$(‘.demo’).demo();});

}(jQuery));(function ($) {

$.each(ABC.Prototypes, function (widgetName, widgetPrototype) {$.widget(‘abc.’ + widgetName, widgetPrototype);

});}(jQuery));

ABC = {};(function ($) {

ABC.Prototypes = ABC.Prototypes || {};ABC.Prototypes.demo = {

_create: function () {if (this. _getInstanceCount() === 1) {this._attachListeners();}},_getInstanceCount: function () {return $(‘:abc-demo’).length;},_attachListeners: function () {$(‘body’).on(‘click.demo’, ‘:abc-demo’, $.proxy(this._clickHandler, this));},_clickHandler: function () {console.log(this._getInstanceCount() + ‘ demo widgets instantiated!’);},destroy: function () {if (this._getInstanceCount() === 1) {$(‘body’).off(‘.demo’);this._super();}}

};$(function () {

$(‘.demo’).demo();});

}(jQuery));(function ($) {

$.each(ABC.Prototypes, function (widgetName, widgetPrototype) {$.widget(‘abc.’ + widgetName, widgetPrototype);

});}(jQuery));

module(‘demo core’);

test(‘_getInstanceCount’, 2, function () {var container = $(‘<div></div>’).appendTo(‘body’),

deferred = $.Deferred(),myDemo;

stop();

deferred.done(function (instanceCount) {equal(instanceCount, 1, ‘Returns proper value with 1 instance’);}).fail(function () {ok(false, ‘Returns proper value with 1 instance’);}).always(function () {container.remove();start();});

equal(ABC.Prototypes.demo._getInstanceCount(), 0, ‘Returns proper value with no instances’);

myDemo = $(‘<div></div>’).appendTo(container).on(‘democreate’, function () {deferred.resolve(ABC.Prototypes.demo._getInstanceCount());}).demo();

setTimeout(function () {deferred.reject();

}, 250);});

module(‘demo core’);

test(‘_getInstanceCount’, 2, function () {var container = $(‘<div></div>’).appendTo(‘body’),

deferred = $.Deferred(),myDemo;

stop();

deferred.done(function (instanceCount) {equal(instanceCount, 1, ‘Returns proper value with 1 instance’);}).fail(function () {ok(false, ‘Returns proper value with 1 instance’);}).always(function () {container.remove();start();});

equal(ABC.Prototypes.demo._getInstanceCount(), 0, ‘Returns proper value with no instances’);

myDemo = $(‘<div></div>’).appendTo(container).on(‘democreate’, function () {deferred.resolve(ABC.Prototypes.demo._getInstanceCount());}).demo();

setTimeout(function () {deferred.reject();

}, 250);});

module(‘demo core’);

test(‘_getInstanceCount’, 2, function () {var container = $(‘<div></div>’).appendTo(‘body’),

deferred = $.Deferred(),myDemo;

stop();

deferred.done(function (instanceCount) {equal(instanceCount, 1, ‘Returns proper value with 1 instance’);}).fail(function () {ok(false, ‘Returns proper value with 1 instance’);}).always(function () {container.remove();start();});

equal(ABC.Prototypes.demo._getInstanceCount(), 0, ‘Returns proper value with no instances’);

myDemo = $(‘<div></div>’).appendTo(container).on(‘democreate’, function () {deferred.resolve(ABC.Prototypes.demo._getInstanceCount());}).demo();

setTimeout(function () {deferred.reject();

}, 250);});

module(‘demo core’);

test(‘_getInstanceCount’, 2, function () {var container = $(‘<div></div>’).appendTo(‘body’),

deferred = $.Deferred(),myDemo;

stop();

deferred.done(function (instanceCount) {equal(instanceCount, 1, ‘Returns proper value with 1 instance’);}).fail(function () {ok(false, ‘Returns proper value with 1 instance’);}).always(function () {container.remove();start();});

equal(ABC.Prototypes.demo._getInstanceCount(), 0, ‘Returns proper value with no instances’);

myDemo = $(‘<div></div>’).appendTo(container).on(‘democreate’, function () {deferred.resolve(ABC.Prototypes.demo._getInstanceCount());}).demo();

setTimeout(function () {deferred.reject();

}, 250);});

Wrap it up already, will ya?

Richard Lindsey @Velveeta http://conqueringtheclient.com/

ONLY MAKE COMPONENTS AS LARGE AS THEY NEED TO BE / KEEP THEM AS DECOUPLED AS POSSIBLE / CONSUME DOWNWARDS, COMMUNICATE UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

ONLY MAKE COMPONENTS AS LARGE AS THEY NEED TO BE / KEEP THEM AS DECOUPLED AS POSSIBLE / CONSUME DOWNWARDS, COMMUNICATE UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

ONLY MAKE COMPONENTS AS LARGE AS THEY NEED TO BE / KEEP THEM AS DECOUPLED AS POSSIBLE / CONSUME DOWNWARDS, COMMUNICATE UPWARDS

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

DECORATE THE FACTORY, BUT BE CAREFUL ABOUT TYING TO IMPLEMENTATIONS.

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

MAKE FUNCTIONS & OPTIONS GRANULAR AND ROBUST FOR POTENTIAL OVERRIDES.

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

TEST, TEST, AND TEST! MAKE EVERY ATTEMPT TO ENSURE BACKWARD COMPATIBILITY FOR CONSUMERS.

Richard Lindsey @Velveeta http://conqueringtheclient.com/

Wrap it up

already…

thanks!Presentation available online: http://bit.ly/jqwidgets

Richard Lindsey @velveeta http://conqueringtheclient.com/PLATFORM FEE ARCHITECT | THE ADVISORY BOARD COMPANY