Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Getting the Most Out of jQuery Widgets

4,806 views

Published on

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

Published in: Technology, Business
  • Be the first to comment

  • Be the first to like this

Getting the Most Out of jQuery Widgets

  1. 1. Richard Lindsey @Velveeta http://conqueringtheclient.com/ PLATFORM FEE ARCHITECT | THE ADVISORY BOARD COMPANY jQuery Widgets GETTING THE MOST OUT OF
  2. 2. Let’s say we’re making Widgets… Richard Lindsey @Velveeta http://conqueringtheclient.com/
  3. 3. What’s a Widget? Richard Lindsey @Velveeta http://conqueringtheclient.com/
  4. 4. ELEMENTS / COMPOUNDS /CELLS / ORGANISMS Richard Lindsey @Velveeta http://conqueringtheclient.com/ Think small. Think modular.
  5. 5. Communicate through events. KEEP COMPONENTS DECOUPLED / MAKE THEM SUBSCRIBE AND RESPOND Richard Lindsey @Velveeta http://conqueringtheclient.com/
  6. 6. Communicate through events. KEEP COMPONENTS DECOUPLED / MAKE THEM SUBSCRIBE AND RESPOND Richard Lindsey @Velveeta http://conqueringtheclient.com/
  7. 7. 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/
  8. 8. 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/
  9. 9. 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/
  10. 10. 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/
  11. 11. $.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(); });
  12. 12. $.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(); });
  13. 13. $.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(); });
  14. 14. $.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(); });
  15. 15. $.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(); });
  16. 16. Richard Lindsey @Velveeta http://conqueringtheclient.com/ BAD IDEA AHEAD
  17. 17. Decorate ALL the functions! Richard Lindsey @Velveeta http://conqueringtheclient.com/
  18. 18. Richard Lindsey @Velveeta http://conqueringtheclient.com/ MODIFY THE FACTORY FUNCTION IF YOU NEED TO Decorate ALL the functions!
  19. 19. 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; });
  20. 20. 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; });
  21. 21. 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; });
  22. 22. 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; });
  23. 23. 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; });
  24. 24. Richard Lindsey @Velveeta http://conqueringtheclient.com/ ALWAYS TRY TO USE PUBLIC API FOR FORWARD COMPATIBILITY Decorate ALL the functions!
  25. 25. Richard Lindsey @Velveeta http://conqueringtheclient.com/ WHO CARES ABOUT INTERNAL IMPLEMENTATIONS? Feel free to mix it up.
  26. 26. 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
  27. 27. $.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)); } });
  28. 28. $.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)); } });
  29. 29. $.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)); } });
  30. 30. $.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)); } });
  31. 31. Make it testable! Richard Lindsey @Velveeta http://conqueringtheclient.com/
  32. 32. 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/
  33. 33. 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/
  34. 34. Richard Lindsey @Velveeta http://conqueringtheclient.com/ PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY expose it!
  35. 35. Richard Lindsey @Velveeta http://conqueringtheclient.com/ PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY expose it!
  36. 36. Richard Lindsey @Velveeta http://conqueringtheclient.com/ PUBLIC FUNCTIONS SHOULD HAVE UNIT TESTS / STORE PROTOTYPES IN OBJECT NAMESPACES / TEST LOGICAL FUNCTIONS SEPARATELY expose it!
  37. 37. 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));
  38. 38. 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));
  39. 39. 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));
  40. 40. 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); });
  41. 41. 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); });
  42. 42. 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); });
  43. 43. 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); });
  44. 44. Wrap it up already, will ya? Richard Lindsey @Velveeta http://conqueringtheclient.com/
  45. 45. 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…
  46. 46. 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…
  47. 47. 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…
  48. 48. DECORATE THE FACTORY, BUT BE CAREFUL ABOUT TYING TO IMPLEMENTATIONS. Richard Lindsey @Velveeta http://conqueringtheclient.com/ Wrap it up already…
  49. 49. MAKE FUNCTIONS & OPTIONS GRANULAR AND ROBUST FOR POTENTIAL OVERRIDES. Richard Lindsey @Velveeta http://conqueringtheclient.com/ Wrap it up already…
  50. 50. TEST, TEST, AND TEST! MAKE EVERY ATTEMPT TO ENSURE BACKWARD COMPATIBILITY FOR CONSUMERS. Richard Lindsey @Velveeta http://conqueringtheclient.com/ Wrap it up already…
  51. 51. thanks! Presentation available online: http://bit.ly/jqwidgets Richard Lindsey @velveeta http://conqueringtheclient.com/ PLATFORM FEE ARCHITECT | THE ADVISORY BOARD COMPANY

×