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

Developing a Twitter Flight Edge

3. Twitter Flight - Event-Driven Components

As a single-purpose library, Twitter Flight aims to solve one and only one problem - attaching complex behaviors to on-page HTML components in a lightweight and reusable way. Flight uses an AMD (Asynchronous Module Definition) package structure to separate core functionality into discrete parts that can be used even in the absence of a defined Flight component. This chapter visits each of the modules in the Flight package and explains their use and role in the library.

What is a "Component"?

The Flight readme defines a component as "... a constructor with properties mixed into its prototype". More specifically, a component is a DOM element with a well-defined (and well-confined) scope that has added behaviors attached. By default, components have basic event handling methods for attaching and detaching listeners as well as triggering events. They also have several convenience functions for handling configurable attributes and using attributes as selectors for binding events and modifying child elements or components.

You can think of a component as a discrete widget on a page with its own behaviors. Well-defined components have a sharp context separation between what is and what is not a part of the component. Twitter has a component for their timeline, which handles rendering and removing tweets from the timeline; a component for the "Who to follow" sidebar, which handles the recommended users for you to follow; and many more.

Creating Components

There are two functions involved in creating components: the component constructor and the component initializer. The constructor is responsible for defining the behaviors of a component and setting up the default attributes, and the initializer is run for each DOM element to which the component is attached. Methods bound to the constructor will be available on the component as prototype functions.

The method for creating components, defineComponent, takes a list of mixins and applies them one by one to your component's constructor. In addition to the given component constructor function and any mixins you specify, it also adds some core mixins to provide universal component functionality for event handling, attributes, and function metaprogramming.

Component constructors can also define a set of default attributes for component instances. These attributes are often selectors for descendent DOM elements contained within a component to which different event listeners are attached; both the select and on method have variants that accept an attribute name instead of a normal selector and use the attribute value as the selector. Attributes can also be null to force you to define them at attachment time and throw an error if absent:

0001:    // Define the component constructor
0002: function SelectableListComponent() {
0003: // Assign default values for selector attributes
0004: this.defaultAttrs({
0005: "menuItems": "li",
0006: "cancelButton": "button.cancel"
0007: });
0008:
0009: // Define prototype functions for the component
0010: this.selectMenuItem = function() {...};
0011: this.reset = function() {...};
0012:
0013: // Define the component initializer
0014: this.after('initialize', function() {
0015: this.on('click', {
0016: "menuItems": this.selectMenuItem,
0017: "cancelButton": this.reset
0018: });
0019: });
0020: }
0021: // Create the component
0022: var ListComponent =
0023:defineComponent(SelectableListComponent);

Attaching Components to DOM Elements

Once defined, components need to be given a set of elements to initialize with the component behaviors. The component returned from defineComponent has an attachTo method that takes a selector or set of elements and applies the component initializer to each of them. The attachTo method also accepts an optional attributes hash that is combined with the default attributes of a component to customize general components for different situations. For example, the above component could represent a list of items, like a regular <ul> element with <li> children, or it could also apply to a <nav> element with each link representing a "menu item".

0001:    $(document).ready(function() {
0002: ListComponent.attachTo('.some-component');
0003: ListComponent.attachTo('nav', {
0004: "menuItems": "a.nav-item"
0005: });
0006: });

Attaching Event Listeners

Component event listeners come in two major flavors: events within the component (clicks, keypresses, etc.) that the component synthesizes into some complex event, and global event listeners that respond to page-wide events. We've already seen an example of component level event listeners with the SelectableListComponent; the component listens for click events on menu items and cancel buttons and provides some behavior to respond to those events. Binding callback functions to listen for events on descendant elements is so common in Twitter Flight that the on method provides a compact way to assign callback functions to attribute selectors.

Global event listeners are Flight's solution to communication between disjoint components. Suppose, for example, you have an aggregated list of RSS feeds (possibly represented by out SelectableListComponent), and off in the sidebar you have another component that displays some stats about your feeds (the last time they were refreshed, the latest item in each, etc.). If a user clicks the refresh button near the list, they would expect both the aggregate list feed and the stats to update. If we relied solely on DOM element hierarchies to communicate information between components, the aggregate list would never be able to communicate to the stats component that an update had occurred, and the page would be out of sync. Instead, both components register callbacks with the document object to listen for update events. Other languages and frameworks commonly refer to this construct as an "event bus" or "event aggregator". The document object serves as an excellent proxy for this functionality because all events triggered anywhere in the DOM eventually bubble up to document, allowing components to communicate with each other without enforcing a structural hierarchy:

0001:    function RSSFeed() {
0002: this.after('initialize', function() {
0003: this.on(document, 'feedUpdate',
0004:this.updateFeedListWithNewItems);
0005: });
0006: }
0007: function RSSStats() {
0008: this.after('initialize', function() {
0009: this.on(document, 'feedUpdate',
0010:this.recomputeRSSStats);
0011: });
0012: }

You can also remove attached events by calling the off method with the same parameters as on; this is most often used for toggling behaviors like enabled/disabled where the component should stop responding to events until something re-enables it.

Triggering Events

Restricting components to just native DOM elements would result in a lot of filtering to extract useful events from the noise. Flight offers a convenience method trigger to facilitate creating and firing custom events. The only required parameter is the event type string (e.g. feedUpdate); it also accepts an optional object as the event payload to communicate data between components, and can also fire events at other DOM elements.

Custom events can also have default actions that are invoked if no listener in the call chain prevents it by calling event.preventDefault(). This allows you to define behaviors for a component that ancestor components can verify or supersede if necessary. Like form validation and replacing links with AJAX functionality, default behaviors open up a new way of architecting your app for a more robust product.

Destroying Components

In theory, event listeners should be automatically cleaned up when the attached element is removed from the DOM. This assumption is not safe; as mentioned before, some components may register event listeners to the global document object, which exist outside the scope of the removed DOM element. If a component's element is removed from the DOM without having its global listeners detached, dangling callback functions can be left on the document object. At best, this behavior causes a memory leak; at worst, your application will start throwing errors as event listeners run on now-defunct components.

Flight provides a mechanism to completely destroy a component and all registered callback functions via the teardown method. This function can be invoked at either the instance level, destroying a single instance of a component; the component level, destroying all instances of a specific component; or at the root level, destroying all instances of all components on a page. Each of these invocations has specific use cases, though for our purposes we will attempt to design our application with a static set of components to avoid the need to destroy any instances of them.

What is a mixin?

A Flight Mixin may be better understood as an analogue to other popular web languages, depending on your development background. For Ruby developers, a Flight mixin is just like a Ruby mixin; you could conceivably write an Enumerable mixin that provided the same functionality as the Ruby mixin of the same name. JavaScript and PHP developers familiar with the direction of their respective languages may know mixins better as "traits". Other developers can think of mixins as similar to multiple inheritance, where classes may inherit properties and methods from more than one parent class.

Creating mixins

Writing a reusable mixin is as easy as writing a function to attach methods and properties to a target object. If that sounds familiar, it's because we've already written a mixin in this chapter: the SelectableListComponent can be used as both a standalone component and a mixin to other components that have similar selectable lists in their structure! You may have wondered why the defaultAttrs method of components accepts null as a value and throws an error if not provided. While not often used by normal components, required attributes are frequently used by mixins to assert the need for some valid attribute to be defined by inheriting components or when components are instantiated and attached to DOM elements.

Mixins commonly use the advice module to prevent clobbering functions defined by the component. We'll see later how well-behaved mixins can transparently add behaviors to inheriting components without requiring either the mixin or the component to know about the implementation of the other.

Using mixins with regular objects

Mixins represent an integral part of Twitter Flight. Under the hood, defineComponent adds its own mixins to any component constructor provided to attach behaviors universal to all components, like before and after from the withAdvice mixin, and onoff, and trigger from the private withBaseComponent mixin. Mixins are not restricted to only Twitter Flight components; using the compose module, mixins may be added to any arbitrary object to give enhanced functionality similar to a component. Simply call compose.mixin(yourObject, [yourMixins]) and the behaviors will be attached to yourObject as you expect.

Composing functions with "advice"

We have mentioned this module many times in this overview chapter. Flight makes frequent use of it internally and strongly suggests using it in your components and mixins. The advice module is a set of functions (before, after, and around) used to compose individual methods into complex functions. Looking again at our SelectableListComponent, we see the initializer function defined as this.after('initialize', function() {...}). This allows the SelectableListComponent to be used in conjunction with any number of well-behaved mixins that may attach behaviors to the initialize method as well; they will just sequentially attach more functions to the method as needed to ensure full initialization.

The functions available to components are different from the ones exposed via the AMD module. Those methods accept two functions, a base function and the "advice" function, and return a new function that composes the arguments into a complex higher-order method. Both before and after will pass the arguments list of the complex function to each constituent piece, but around expects you to define exactly when the base function should be invoked and with which arguments.

before & after

These methods result in a new function that executes the base function and the advice in the specified order. The arguments list and calling context are the same in both functions. As standard methods, these will guarantee that both the base method and the advice method are invoked; as component methods, however, they do not require that the base method exist to attach the advice method. The advice method will be returned as the "composite" function in that case.

0001:    // Before showing an element, render its 
0002: // contents first
0003: this.before('show', this.render);
0004: // After resetting a form component,
0005: // reset the error messages
0006: this.after('reset', this.clearErrorMessages);

Be aware that, though before and after sound like event-based methods, they do not interact with or function like event methods. As component methods, they replace the named function specified with a composite function. In the render/show case, the show method would be replaced with a function that invokes render and then show.

around

Unlike before and after, the around advice method leaves it up to you, the developer, to define when a base method is invoked - if it is invoked at all - and with what arguments. This permits some freedom with the types of composite methods available with around advice; form validation could be implemented as an addition to the submit method, which could avoid calling the base submit method if validation fails. This advice is more commonly used to set up some context to the component or base function and tear down the changes after the base function is invoked.

0001:    // Add custom form validation (since you 
0002: // don't necessarily have to call the
0003: // wrapped function)
0004: this.around('submit', this.validate);

Conclusion

In this chatper, we toured the functionality Flight offers us when building applications. The core unit in Flight is a "component," an event-driven module that listens to events bubbling through its own markup or globally across the page. For users more familiar with a traditional MVC framework like Backbone.js or Ember.js, components replace the "controller" and "view" functionality, though many of those functions may be conferred to a component by general-purpose mixins and dependencies injected into a component's attributes. In the next chapter we will cover the basics of events and Flight components, before diving in and starting to write our sample application!

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

You should refresh this page.