English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing a Twitter Flight Edge

8. Mixins and Advice

In this chapter, we will:

  • Explore adding common functionality to components via mixins
  • Refactor our existing components to use mixins

Now that we've constructed a fully-fledged and fully-featured application with Flight, from UI components to data providers and services, it's time to look at the other major component of the library: mixins. While components can be written in isolation from each other with no shared code except via message passing, you may find that some of your components need to accomplish similar tasks. Mixins provide a way to extend components with common functionality. Whereas traditional libraries like YUI and Backbone suggest you extend some parent class to extend functionality, Flight mixins behave more like mixins from Ruby, where multiple mixins can be included in a component and the methods of each are made available. Let's start refactoring our application to use mixins and explore their use in Flight applications.

Adding Functionality

Let's first look at our UI components; both rely on some parent container being present in the page, then insert some markup on initialization to make the component "ready". We can extract this behavior into a separate mixin, which we will call withMarkup, to let us add a template attribute to a component and have the markup inserted into the container node when the component is initialized. As always, we'll start with a test that verifies the markup defined for the template attribute is, in fact, inserted:

0001:    describeMixin('mixin-markup', function() {
0002: it("should insert the markup in the 'template' attribute when the
0003:component is initialized", function() {
0004: setupComponent({
0005: "template": '<span class="ok"></span>'
0006: });
0007: expect(this.component.$node.html()).toBe('<span class="ok"></span>');
0008: });
0009: });

Like component tests, most of the boilerplate for testing mixins is handled by Flight Jasmine's describeMixin and setupComponent functions. This test will automatically create a dummy component that includes our mixin; all we need to do is give it a template attribute and make sure the node contents match. Implementing the mixin should be fairly straightforward; when the component initializes, just set the inner HTML of the attached node, right?

0001:    define(function(require) {
0002: function withMarkup() {
0003: this.initialize = function() {
0004: this.$node.html(this.attr.template);
0005: };
0006: }
0007: return withMarkup;
0008: });

This gets our test to pass, but we could never use this in a component. Mixins are invoked over the component prototype after the main component definition; in this code, we've clobbered the original initialize method, and components that include this mixin will not initialize properly. Instead, we need to compose the original initialize method with the augmented behavior this mixin contains. In previous chapters, I mentioned that idiomatic Flight code includes initialization code written as this.after('initialize', function(){...});, and now we see why - overwriting the primary initialization method can lead to unexpected behavior. Two of the Flight core mixins (events and registry) add their own initializers to every component, so in addition to obliterating our own component initializers, we have also interfered with Flight's component feature itself. Avoiding this situation is so central to Flight that testing it is redundant; we'll just fix our code to be a more idiomatic mixin rather than write a test to verify we haven't done something idiotic like overwrite the initialize method. We have several options for function composition; we've already seen after, which will invoke our code after the named method, but now we can also choose before (to run our code first) and around (to run code both before and after the method). Since inserting the markup is fairly important for our component to function properly, we'll make sure it's inserted as soon at the component is initialized.

0001:    function withMarkup() {
0002: this.before('initialize', function() {
0003: this.$node.html(this.attr.template);
0004: });
0005: }

Our test is still passing, and now the components including this mixin will initialize properly. Let's add our mixin to our UI components to simplify their initializers:

0001:    // in app/js/feed-manager.js
0002: function FeedManager() {
0003: this.defaultAttrs({
0004: "template": require('text!tmpl/feed-manager.html')
0005: });
0006: // remove `this.$node.html(this.attr.feedManagerTemplate)` from initialize
0007: }
0008: return defineComponent(FeedManager, require('mixin-markup'));
0009:
0010: // in app/js/feed-aggregator.js
0011: function FeedAggregator() {
0012: this.defaultAttrs({
0013: "template":require('text!tmpl/feed-aggregator.html')
0014: });
0015: // remove `this.$node.html(this.attr.aggregatorTemplate)` from initialize
0016: }
0017: return defineComponent(FeedAggregator, require('mixin-markup'));

Our component initialize methods are now focused more clearly on setting up the component event listeners, and the component markup insertion is extracted to a simple attribute. Additionally, our components are now more consistently customizable from our application boot script: if we wanted to show multiple feed lists, or create a second dialog-friendly feed manager, we could simply change the template attribute when attaching the components and the alternate markup would be inserted for us. Speaking of templates, let's revisit our oversimplistic "templating" system and replace it with a mixin.

Adding Component Methods

Mixins are obviously not limited to just adding functionality to existing component methods. They can also add common behaviors to components: a visibility mixin could add show and hide methods that toggle the display property of the component root node; an expando mixin could manage toggling an expandable sub-element of a component; or a data mixin could simplify sending AJAX requests from your component. Each of these mixins needs to add methods to the component, and our template mixin is no different. It will offer one convenience method, template, which will perform a simple search and replace on a named attribute with a given data hash.

0001:    describeMixin('mixin-template', function() {
0002: describe("#template", function() {
0003: it("should format the named template with the
0004:supplied object", function() {
0005: setupComponent({
0006: "nameOfTemplate": '<div class="test {className}">
0007:{contents}</div>'
0008: });
0009: var templateReturn = this.component.template('nameOfTemplate', {
0010: className: "arbitrary-class",
0011: contents:
0012:"random content"
0013: });
0014: expect(templateReturn).toBe('<div class="test arbitrary-class">random content</div>');
0015: });
0016: });
0017: });

Personally, when writing tests for mixins that attach methods to a component, I prefer to associate the tests for each method into a single suite. This will allow us to run all the tests for a specific method in the browser if we need to debug the method and ignore the unrelated methods. This test specifically ensures that the template method correctly performs a search and replace on the named attribute and inserts the data from the hash. We'll implement it with the string replace method using a function as the replacement value:

0001:    define(function(require) {
0002: function withTemplating() {
0003: this.template = function(templateName, obj) {
0004: return this.attr[templateName].replace(/(?:\{([^}]+)\})/g,
0005:function(_, property) {
0006: return obj[property];
0007: });
0008: };
0009: }
0010: return withTemplating;
0011: });

This function fetches the template string from the component attributes and runs a search and replace over all the brace-wrapped keys, and then performs a property lookup on the data hash after extracting the keys. We can use this mixin in both of our UI components to replace the manual templating with a simpler template call:

0001:    // in app/js/feed-aggregator.js
0002: function FeedAggregator() {
0003: this.defaultAttrs({
0004: "optionTemplate": '<option value="{feedUrl}">{title}</option>',
0005: "itemTemplate": require('text!tmpl/feed-item.html')
0006: });
0007: this.insertOption = function(feed) {
0008: // ...
0009: this.select('filterSelector').
0010: append(this.template('optionTemplate', feed));
0011: };
0012: this.insertOption = function(feed) {
0013: // ...
0014: this.select('filterSelector').
0015: append(this.template('optionTemplate', feed));
0016: };
0017: }
0018: return defineComponent(FeedAggregator, require('with-markup'), require(
0019:'with-template'));
0020:
0021: // in app/js/feed-manager.js
0022: function FeedManager() {
0023: this.defaultAttrs({
0024: "feedListItemTemplate": require('text!tmpl/feed-manager-feed.html')
0025: });
0026: this.addFeed = function(event, feedData) {
0027: this.select('feedList').
0028: append(this.template('feedListItemTemplate', feedData));
0029: };
0030: }
0031: return defineComponent(FeedManager, require('with-markup'),
0032:require('with-template'));

We've replaced all of the code that inserted data into the markup via selectors with a single template call, and defined our templates (either inline as with the optionTemplate or by loading the template via require). All that's left is to update the templates with the proper brace-delimited keys and all of our tests for both FeedAggregator and FeedManager will continue to pass.

Conclusion

Our app is finally finished! In this chapter, we revisited the components written over the course of this book and extracted common functions into separate mixins. First, we wrote a mixin that augmented component behavior with Flight's advice methods to insert markup on initialization. We then created a mixin to make a templating method available on our components to facilitate updating the DOM when we receive data.

Flight isn't a large library, but it is extremely powerful; with just a few hundred lines of code, we've managed to make a fully-featured rich web application to read RSS feeds, and all using browser-based JavaScript. It works well for small applications like ours with just a few components, as well as larger-scale applications like Twitter with hundreds of components. 

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.