Getting the Most Out of jQuery Widgets

4,655 views

Published on

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

Published in: Technology, Business
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total views
4,655
On SlideShare
0
From Embeds
0
Number of Embeds
2
Actions
Shares
0
Downloads
21
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide
  • Hi, I’m Richard Lindsey, a front-end architect working for the Advisory Board Company here in Austin. We’re a healthcare consulting company that deals a lot with metrics and regurgitating data out onto the screen in various forms, and we utilize the jQuery widget factory pretty extensively to that end. If you’re not familiar w/ the widget factory, jQuery UI is a separate add-on library, based on and extending jQuery, and providing a pretty nice factory method to create your own library of visual widget components. I’m here to share some tips and tricks on working with that factory and in general component architecture… So…
  • Let’s say we’re making widgets…
  • What’s a Widget? In the context of front-end engineering, it’s simply a modular, reusable, self-contained package that handles the visualization of some UI component, as well as any behavior and logic that’s part of that component’s interaction… Think of things like typeaheadautocompleters, or split buttons, or tabbed views, or any number of other things, and those can all be thought of as widgetized components… So what kinds of things should we be considering when we’re developing these components?
  • Think small… Think modular… You should be trying to build your components so that they fulfill a single, specific need. Anything too complex should be broken down into smaller components that can be used in other facets of your application… Consider the autocompleter: it could be made up of, at a minimum, an input element, some form of data-fetching utility, whether that’s via ajax or some client-side data set, and something to render the suggestions in a dropdown format… If you build all of that into 1 widget, you can’t reuse those bits and pieces in anything else… So whenever possible, think as small as possible… This is an analogy for how your widgets should kind of come together. You can think of the most basic components: text boxes, checkboxes, select elements, overlays, whatever, as elements. From there, you can start coupling them to other elements to form larger and more complex compound components… At that point, you can start thinking larger, about how compound widgets can come together to form cells and organisms, being synonymous with the workflow of an entire page, or even of an entire application…
  • You should be trying to keep your components as directly decoupled as possible. What does this mean? Well, in our autocompleter example, when something’s typed into the textbox, we can have the input itself tell the data-loader “hey, I have updated data for you to provde me matches against”, and the data-loader can fetch them and render the dropdown list itself, and when something’s selected from it, the dropdown can tell the input “here’s the complete string you should now populate yourself with”… However, that requires these components having direct knowledge of each other everywhere you want to do something like this… Instead, you should make these individual pieces broadcast events whenever they have updates that other interested parties might want to know about…
  • Now, once you have them broadcasting events, you can have them subscribing to each others’ events, and feeding data to them, but that still doesn’t solve the problem of them being directly coupled to each other…
  • You should be bundling these smaller modules up and wrapping them in some kind of parent widget that can play traffic cop between them all… The parent should be responsible for subscribing to their events, and for figuring out what needs to be done with that data: whether it should use it directly somehow, or funnel it into one of those child widgets to be acted upon…
  • You should also be providing some kind of public API on all of your widgets that you can use for calling into them and handing off data. All jQuery UI widgets have at least a partial API provided by the factory: enable and disable, option, widget, and destroy... Whatever purpose your widget serves should have a robust enough API that nobody has to try to hack their way in through the instance data that’s bound to that element, as I’m sure many of you have had to do in the past…
  • Always try to keep in mind that dependency relationship… None of the element-level components are dependent on the parent, it’s the parent that’s dependent on them… As such, any direct references to those widgets should be done in a top-down fashion, with the parent referencing its children and not the other way around…
  • Here, I say each layer should listen for events, because you never know when some other widget is going to be consuming a parent widget you’re writing here… That’s the beauty of this design methodology, you don’t really have to care… You just worry about what you’re listening for from your dependencies, and anytime you have something interesting to publish, throw it out into the world as a new event of your own, and other consumers can now consume those events, with your widget never knowing who’s listening, and never having to write specific logic to deal with it…
  • Here’s a simple example of what we might find in an autocomplete component…
  • We declare some dependencies on other, smaller widgets…
  • We then bind listeners to those child widgets…
  • We supply a couple of functions for interfacing directly with this widget from higher levels…
  • And finally, we set up some event triggers of our own, to broadcast things out into the world… So here’s what a sample implementation could look like in the browser…
  • Now, so far this whole talk has been about keeping things nice and abstracted, using public API’s so you don’t have to concern yourself with how things are implemented internally… For the next 2 minutes, I want you to throw out everything I’ve said… See, all those things I’ve been saying, those are the way things work in an ideal world, and there are times when I’m sure we’ve all had to break the rules of good development patterns to satisfy some business need…
  • I found myself having need of supplying our product’s consumers with a more robust event-publishing system… There are times our developers wanted to be alerted to when a widget had built some dropdown list, or some other internal mechanism had changed, and we just didn’t provide that many events… Our product releases are on a monthly schedule, meaning that when they found a need for something we weren’t currently publishing, their request would have to wait up to a month, *if* we were able to fit that feature request into our current release… Instead, I decided to start decorating all of the prototypes for all of our published widgets, so that they could tell the widget to start auto-publishing an event before or after any function it used… Now we can still handle custom event requests that may include extra data being passed as part of an official custom event request, but the majority of those requests can be handled by just using this new event auto-publishing feature…
  • So, if you ever have need of decorating an entire prototype somehow, you should do it before it’s run through the factory, meaning you’ll have to decorate the factory function itself to keep this process as transparent as possible…
  • Here’s an example of how we might decide to decorate our factory function…
  • Here you can see us overwriting the widget factory function and creating a new version of it to used…
  • We iterate over our prototype methods, surround the execution of each one with a couple of logical checks, and fire some functions if they pass…
  • We also store off the return value of our original function, to return at the end of all that…
  • We then pass our decorated prototype through the original factory method to create the widget class definition, and port any extra properties over to our new factory function… There are a couple of extra functions that are stored as properties on the factory function object, like extend and bridge, so make sure you don’t forget this step or you *will* run into errors…
  • While this does allow people to tie event publishers to internal private functions, I can’t state strongly enough that you should avoid that development practice whenever possible… Always try to use the public API for forward compatibility, because you never know when internal implementations are going to change from one release to another… That public API acts as a sort of contract between the publisher and consumers of a widget, which should stay as consistent as possible from release to release, but the internal private functions have no such guarantee…
  • As long as your function interfaces are consistent, meaning the input and output formats stay the same, does it really matter how the guts of that function are implemented? Not really… And if we want to swap out functional implementations, we have a couple of different options for doing so…
  • The widget factory itself allows you to override any method, private or not… One nice thing about this method is that if directly affects the widget’s prototype, and any instances that are already created… It also maintains a pointer back to the original version of that function, which can call with the super and superApply methods, which allows us to simulate some actual inheritance, which is awesome… The other method is to expose some of your functions as options that can be modified at runtime… This only affects the specific instance you’re modifying that option on… This allows for a lot more flexibility in the behavior of your widgets…
  • So, here’s an example where we have a generic dataloader widget that just adds a loading class to the element, which can be used to modify the mouse cursor to a busy state, or white out the area being loaded, or whatever… Let’s say we wanted to keep that same behavior for all ajax functionality, but allow Backbone to manage the fetching of our collection data…
  • As you can see, our fetch function takes no inputs, and returns the promise object that’s coming from the $.ajax function…
  • This means that we can integrate Backbone’s fetch method by creating our own deferred object, running our fetch, and returning that deferred’s promise… Whenever our success or error functions are called from Backbone, we resolve or reject as appropriate, and if resolving, pass our collection data to any callbacks that have been bound to that promise, and business proceeds as normal…
  • You’ll also see that we have a success function option we’ve added here, which by default just outputs a JSON string of our result set from that ajax request, but which we can override when we instantiate this widget, to have it run those results through a Handlebars template for output instead…
  • Now, how many of you know that you should be unittesting your front-end code? …Ok, so, how many of you *actually* unit test your front-end code?We should be writing our code in such a way that it *is* testable… That means you should try to limit the amount of anonymous functions you’re passing back and forth…
  • Does a function perform some kind of logical operation, or calculation? If so, you may not even need to create an instance of your widget to test that…
  • Is it part of your widget’s public API, such that your consumers are going to expect a level of continuity from release to release? This one is especially important… And if you’re designing your interface to expect certain inputs and give back certain outputs, you should be able to easily test those conditions…
  • All public functions should have unit tests written to ensure that they continue to function property from release to release. This is going to be one of the biggest indicators of code quality from the perspective of your consumers… If they can’t upgrade your package without having to refactor their code and test for bugs stemming from broken interfaces, one of 2 things is going to happen… They’ll look for another, more reliable solution, or write one themselves… Or, they’ll stop updating your package, and at some point, they’ll probably look for a different solution anyway…
  • Consider storing your widget prototypes in an object namespace, and pass that into the factory, instead of passing an object literal directly. That way you maintain a reference to all of those functions outside of having to actually create an instance of your widget…
  • If a function is strictly a logical check or a calculation, you may be able to test that completely outside the scope of a widget instance, which can make speed up your test runs, as well as decoupling that function’s logic from the context of the widget itself… Decoupling is a good thing, right?
  • Here’s a simple widget class that just checks to see if it’s the first of its kind, and if so, adds a click handler to the body that outputs the number of these widgets on the page…
  • First, you’ll notice that we store the prototype object in this ABC.Prototypes namespace, and iterate over that namespace to create our widget classes…
  • This getInstanceCount does nothing more than scrape the DOM to find the number of these widgets in existence, and return that… It doesn’t use any internal references to do this, and doesn’t need to be run in the context of a widget in order to do its job… And it’s called from a few different places, so you’ll want to make sure this thing doesn’t get broken at some point, which means you’ll want to test it…
  • Here’s a simple test we could write for it…
  • We first call this function directly from our ABC.Prototypes namespace, since we don’t yet have any instances spun up, and we expect it to return a count of 0…
  • We add a create event handler, and instantiate the widget… We execute the getInstanceCount function again and expect it to return 1, and then continue as normal…
  • We also hedge our bets a little bit here, in case anything goes wrong with the widget creation process… We set a quarter-second timeout to auto-fail the test, since something’s gone wrong, and then let the test engine continue… You would also want to write tests to verify that your click handler is functioning as expected, and of course any public functions you’re providing…
  • So let me kind of summarize some of the biggest points here…
  • This one should be obvious, nobody likes doing more work than they have to… Your components shouldn’t do more work than they have to either… Keep them as small as you can while still allowing them to perform the task they’re meant to do…
  • Good component design means keeping those components unaware of each others’ existence, except when dealing with dependencies…
  • Higher-level widgets should consume the events and public functions of their dependencies, and broadcast their own events out into the ether for whoever may be consuming them…
  • If you have to decorate entire prototypes, do it by decorating the factory itself, but be *careful* about tying solutions to internal implementations, they can change at any time!
  • Don’t write monolithic functions… Just like your widget design, your functions should be broken down into granular pieces that can be be overridden if needed… Try to provide a robust-enough public API for your widgets, to keep people from having to force their way in to use private functions…
  • If your consumers are in the audience right now, they should be using your public functions as much as possible, riiiiight? That means they’re gonna expect you not to break their code when they update your package… And *that* means you want to make sure you test those functions as much as you can to make sure their interface is consistent from one release to the next…
  • 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

    ×