In this chapter, we will:
At this point, we have covered the creation and use of components, the core purpose of Twitter Flight. Flight is not, however, a fully-featured framework like Angular or Ember; depending on the needs of your application, you may have to turn to other libraries to provide some functionality not built into Flight. We'll apply the last touches of functionality to our application - persisting feeds between page refreshes - and introduce a third party library to handle the persistence.
As you may have guessed, we will be encapsulating our third party library in its own component. This lets us decompose our requirements into a well-defined set of behaviors that we can then implement. We'll start with the behavior we're trying to achieve - persisting the feeds across page views. Our persistence component should, at page load time, initialize the page with feeds added in prior sessions. In order to do so, it will need to listen for added feeds to include in the list, and listen for removed feeds to exclude them as necessary.
Already we can see how adapting libraries via components will simplify the interface between our application code and the library code. Regardless of how the library is implemented, our component will exhibit the same overall structure as any other; a set of event listeners, bound in the initialize function, that map application events to some set of calls to the library. Our experience in Chapter 5 with interfacing to the Google Feed API will come in handy while implementing our persistence layer.
As an adapter for our third party library, out component should be the gateway between our application and the library code. Exposing too much of the library to the rest of our application could integrate it with our application more than is necessary and make it difficult to swap out later if we ever decide to change which library we want to use. Like the other components we've written, including our library adapter component is a single line change in our application initialization module:
0001: // in js/main.js
0002: define(function(require) {
0003: require('persistence').attachTo(document);
0004: });
To get started with writing the adapter piece of this component, we'll use the same strategy we used for the Google Feeds API to test: write a few (testable) functions that actually manage the application/library interface and restrict our code to using it.
0001: describeComponent('persistence', function() {
0002: beforeEach(setupComponent);
0003: afterEach(function() {
0004: $.storage.removeItem('feeds', 'localStorage');
0005: });
0006:
0007: it("should attempt to store an array of feeds via `storeFeed`", function() {
0008: spyOn($.storage, 'setItem');
0009: this.component.storeFeeds(['firstFeed', 'secondFeed']);
0010: expect($.storage.setItem).toHaveBeenCalled();
0011: expect($.storage.setItem.mostRecentCall.args[1]).toBe(JSON.stringify
0012:(['firstFeed', 'secondFeed']));
0013: });
0014:
0015: it("should retrieve stored items via `getStoredFeeds`", function() {
0016: spyOn($.storage, 'getItem');
0017: this.component.getStoredFeeds();
0018: expect($.storage.getItem).toHaveBeenCalled();
0019: });
0020:
0021: it("should retrieve an object equivalent to the one stored", function() {
0022: this.component.storeFeeds(['a feed', 'another feed']);
0023: expect(this.component.getStoredFeeds()).toEqual(['a feed',
0024:'another feed']);
0025: });
0026: });
The only two functions we need from our choice of persistence library are "set" and "get", so we should be able to confine it to an equivalent pair of functions on our component. At this point, we've made the decision on which library to use: jStorage. Our tests make sure that we call the API correctly and that subsequent set/get calls return the proper value. Much like the Google Feed API, the implementation should be relatively short:
0001: define(function(require) {
0002: var defineComponent = require('flight/lib/component');
0003:
0004: function PersistenceComponent() {
0005:
0001: this.storeFeeds = function(feeds) {
0002: $.storage.setItem('feeds', JSON.stringify(feeds), 'localStorage');
0003: };
0004: this.getStoredFeeds = function() {
0005: return JSON.parse($.storage.getItem('feeds', 'localStorage')
0006:|| '[]');
0007: };
0008:
0009: }
0010:
0011: return defineComponent(PersistenceComponent);
0012: });
0013:
Now that the library adapter code is complete, we can focus on the component's integration into our application. Translating our description of some the component's behavior should be straightforward at this point. Once the application is already running, it doesn't do much more than sit on the document and wait for the user to add and remove feeds. Not surprisingly, we already have application-level events that describe exactly what we're looking for - addFeed and removeFeed. We simply need to listen for these events and synchronize our persistence medium when they are triggered.
0001: describe("while the app is running", function() {
0002: it("should respond to the 'addFeed' event by storing the feed", function() {
0003: this.component.trigger('addFeed', {feedUrl: 'http://feeds.com/rss'});
0004: expect(this.component.getStoredFeeds()).toEqual(['http://feeds.com/rss']);
0005: });
0006:
0007: it("should respond to the 'removeFeed' event by removing the feed from
0008:storage", function() {
0009: this.component.trigger('addFeed', {feedUrl: 'http://feeds.com/rss1'});
0010: this.component.trigger('addFeed', {feedUrl:
0011:'http://feeds.com/rss2'});
0012: expect(this.component.getStoredFeeds().length).toBe(2);
0013: this.component.trigger('removeFeed', {feedUrl:
0014:'http://feeds.com/rss2'});
0015: expect(this.component.getStoredFeeds()).toEqual(['http://feeds.com/rss1']);
0016: });
0017: });
Each of these events needs only one listener to update the local storage. Since we've already encapsulated the code managing our external library into two specific functions, we should use them internally to read and write the updates.
0001: function PersistenceComponent() {
0002: this.storeFeed = function(event, data) {
0003: this.storeFeeds(this.getStoredFeeds().concat(data.feedUrl));
0004: };
0005:
0006: this.removeFeed = function(event, data) {
0007: var currentFeeds = this.getStoredFeeds();
0008: var feedIndex = currentFeeds.indexOf(data.feedUrl);
0009: if (feedIndex > -1) {
0010: currentFeeds.splice(feedIndex, 1);
0011: this.storeFeeds(currentFeeds);
0012: }
0013: };
0014:
0015: this.after('initialize', function() {
0016: this.on(document, 'addFeed', this.storeFeed);
0017: this.on(document, 'removeFeed', this.removeFeed);
0018: });
0019: }
All of our tests pass, and our persistence component is nearly complete. If you load up the application and step through the code, you'll see that it correctly synchronizes local storage with the current set of feeds through all additions and removals. That's the only way you'll see it in action, though; it still needs to notify the rest of the application that feeds exist when the application initializes.
At first glance, this seems like a fairly easy problem to solve. When the application is ready, just fire off a few addFeed events from the persistence component and be done with it. This would certainly work, too; a quick test and implementation can easily get us across the finish line:
0001: it("should emit 'addFeed' events when initialized with stored data",
0002:function() {
0003: // add feeds to the storage
0004: this.component.trigger('addFeed', {feedUrl: 'http://feeds.com/rss1'});
0005: this.component.trigger('addFeed', {feedUrl: 'http://feeds.com/rss2'});
0006: // then tear it down
0007: this.component.teardown();
0008: // and spy on 'addFeed'
0009: var spy = spyOnEvent(document, 'addFeed');
0010: this.Component.attachTo(document);
0011: expect(spy.calls.map(function(call) { return call.args[1].feedUrl; })
0012:.sort()).
0013: toEqual(['http://feeds.com/rss1', 'http://feeds.com/rss2'].sort());
0014: });
// in app/js/persistence.js0001: function PersistenceComponent() {
0002: // ...
0003: this.after('initialize', function() {
0004: // for each stored feed, emit an 'addFeed' event
0005: this.getStoredFeeds().forEach(function(feed) {
0006: this.trigger('addFeed', {feedUrl: feed});
0007: }, this);
0008: this.on(document, 'addFeed', this.storeFeed);
0009: this.on(document, 'removeFeed', this.removeFeed);
0010: });
0011: }
We could call it a job well done right now and have a fully functioning RSS reader. When a user uses the application and adds feeds, the page will properly persist those feeds between page visits. So what's wrong with it? For our application, it's fine; it's just a simple single page app with a few components, some libraries, and some remote data services. Try changing the order in which components are attached and see what happens. It might appear that our initialization code isn't running, but that's not quite true; stepping through the code would show that the persistence component is behaving as expected and keeping the local storage in sync. By changing the order in which components are initialized, we've uncovered an implicit load order dependency - we didn't realize it, but initializing the feed manager and aggregator before the persistence component is mandatory for them to respond to the initializating addFeed events.
When building event-driven applications like this, it can be very easy to accidentally introduce implicit dependencies. Unfortunately, avoiding these types of issues can only be accomplished with diligence and best practices. We have several options available as superior alternatives to component-based initialization. One option is a strategy Twitter uses to initialize their application: a number of "boot" modules that separates application initialization from modules themselves. Alternatively, in the spirit of an event-driven architecture, we can create a new event to fire when all components have been attached. Since our component confines the external library code to two functions, we should try to restrict our app initialization to the component as well. We'll introduce an initializeApp event that components can respond to by performing application-level initialization and trigger it when all components have been attached.
0001: it("should respond to 'initializeApp' by triggering 'addFeed' with stored
0002:feeds", function() {
0003: this.component.storeFeeds(['http://feeds.com/init1.rss',
0004:'http://feeds.com/init2.rss']);
0005: var spy = spyOnEvent(document, 'addFeed');
0006: this.component.trigger('initializeApp');
0007: expect(spy.calls.map(function(call) { return call.args[1].feedUrl; }).sort()).
0008: toEqual(['http://feeds.com/init1.rss',
0009:'http://feeds.com/init2.rss'].sort());
0010: });
This looks like a test we're more familiar with! Instead of bolting on some arbitrary behavior to component initialization, we've transformed our application initialization workflow into the component event/response workflow we've become familiar with. In doing so, we've also separated component initialization from application initialization, which helps us avoid undeclared dependencies between components resulting from attachment/initialization order.
Our application is complete! Users can now add and remove feeds, view a summary of individual entries, and close their browser window without losing their list of feeds. We've abstracted a way to interface with local storage into a single component, and the library code is limited to only a few functions so that swapping in a different library should require very little work. Additionally, we discussed application initialization versus component initialization, and separated the two to avoid implicit attachment order dependencies that could be very difficult to diagnose. While we're finished with our application's features, there's more to Flight than siloed, unique components; in the next chapter, we'll refactor our application to make use of Flight mixins and advice to shorten our component-specific code and extract common functionality.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.