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

Developing a Twitter Flight Edge

5. Data Components and Accessing Remote Data

In this chapter, we will:

  • Explore a new type of component used for managing data
  • Interface our new component with the one written in the previous chapter
  • Use remote services to retrieve data asynchronously within the component paradigm

Now that we've seen how to create components and set up event listeners for user-sourced events, let's add a component to handle application events. Some apps may be entirely client-side and only offer browser-local behavior, but most apps will likely need to communicate with a remote server to access additional data. Twitter needs to send out requests for new tweets, Gmail will need new emails, and our application will need to access RSS feed metadata and items. In this chapter, we will build another type of component - a data component - that will interface with a remote service and provide the data we need.

What Are Data Components?

A common pattern in Flight (read: the pattern used by Twitter and designed into Flight) separates components into two distinct types:

  • UI components, responsible for receiving user input and translating it into application-level events
  • Data components, responsible for receiving these application-level events and procuring the data our UI components need.

The Flight convention for this type of communication relies on appropriate event names, generic enough to work in the context of a wide variety of components, but specific enough to convey the type of data attached to the event. Typically, UI components will emit events prefixed with uiNeeds and data components will listen for them, fetch the remote data, and trigger a similarly-named event prefixed with data. These events normally come in pairs; in our example, the FeedManager will emit uiNeedsFeedInfo and the data component will emit dataFeedInfo.

Requesting Data from UI Components

So far in our app, we have one UI component that allows users to enter URLs for RSS feeds. There's an obvious gap in our application - once the user has entered a valid RSS URL, there's no title to display! We will now implement a way for our FeedManager to emit a request for feed metadata before we write the data component to procure it. We'll start with some tests for our FeedManager to ensure it emits events when a new feed is added and that it properly listens for the data component event to update the (correct) feed title.

0001:    it("should respond to 'addFeed' by emitting a 'uiNeedsFeedInfo' event", 
0002:function() {
0003: var eventSpy = spyOnEvent(document, 'uiNeedsFeedInfo');
0004: this.component.trigger('addFeed', { feedUrl: FEED_URL });
0005: expect(eventSpy).toHaveBeenTriggeredOn(document);
0006: expect(eventSpy.mostRecentCall.data).toEqual({
0007: feedUrl: FEED_URL
0008: });
0009: });
0010: it("should respond to 'dataFeedInfo' by updating the feed title", function() {
0011: var feedUrl = 'http://127.0.0.1/feed-1';
0012: this.submitFeed(feedUrl);
0013: this.component.trigger('dataFeedInfo', {
0014: feedUrl: feedUrl,
0015: title: "The Feed Title"
0016: });
0017: expect(this.component.select('feedItem').filter(function() {
0018: return $(this).find('.url').text() == feedUrl;
0019: }).find('.title').text()).toBe("The Feed Title");
0020: });

Recall from the last chapter that the event emitting behavior should be separated from the work function. At the time, it might have seemed a bit stilted to introduce some indirection between accepting user interaction and responding to it, which we were initially able to accomplish synchronously with little difficulty. Now that there's an asynchronous aspect of the request/response interaction, separating the two function seems a more natural way to handle it. By treating synchronous and asynchronous operations in the same way, we can interchange any synchronous or asynchronous components without difficulty, and our code will be much easier to read since you won't have to question whether or not some workflow contains an asynchronous step. If all of your applications' workflows are event-based, then it doesn't matter if those events are triggered immediately or after some delay.


With two failing tests, it's time to implement the behavior. The FeedManager is already listening for addFeed; that's how we've synthesized the user input in the feed form into an application level event. We'll emit another application-level event, uiNeedsFeedInfo, after we've finished the initial processing of the addFeed event.

0001:    this.addFeed = function(event, feedData) {
0002: ...
0003: // request additional feed data
0004: this.trigger('uiNeedsFeedInfo', feedData);
0005: };

This gets our first test to pass. All that's left is to add a listener for the dataFeedInfo event to insert the feed title into the correct row. We'll add the listener in initialize and write a simple callback to extract the title from the feed data and update our feed row. Updating the correct row highlights one of the many benefits of using the Flight convenience method select; it allows us to quickly go from our component object to the proper set of DOM elements, then use jQuery's chainable traversal functions to narrow down our element set until only the correct .title elements are left:

0001:    this.updateFeed = function(event, feedData) {
0002: this.select('feedItem').filter(function() {
0003: return $(this).find('.url').text() === feedData.feedUrl;
0004: }).find('.title').text(feedData.title);
0005: };
0006: this.after('initialize', function() {
0007: this.on(document, 'dataFeedInfo', this.updateFeed);
0008: });

Note the call signature for the on method; unlike previous invocations, we are attaching this event listener to the root document node so that our component will be notified of any dataFeedInfo events triggered from any component on the page. This code gets our second test to pass, and we now have a properly-evented, properly-tested component that can communicate its needs to any interested provider and listens for updates to that data. Don't forget to commit when you see green tests!

Data Components

Data components, unlike UI components, are generally attached to the topmost document element.

0001:    define(function(require) {
0002: // ...
0003: require('feed-service').attachTo(document);
0004: });

There's no additional markup to write; simply attaching the component to the document should suffice to include our data component into our application. Data components communicate with the rest of the application by listening for specific events triggered anywhere on the page and responding to them by triggering events at the document. Our feed service data component needs to listen for uiNeedsFeedInfo, intepret the feed request, and emit a dataFeedInfo event with the requested data. We can stub out this functionality in our test, and we'll abstract the actual fetch method in our component to make it easier to test:

0001:    describeComponent('feed-service', function() {
0002: it("should respond to 'uiNeedsFeedInfo' events with 'dataFeedInfo'",
0003:function() {
0004: var eventData = spyOnEvent(document, 'dataFeedInfo');
0005: // intercept the 'executeRequest' method and just call through
0006: spyOn(this.component, 'executeRequest').andCallFake(function(url,
0007:callback) {
0008: callback.call(this, {
0009: feedUrl: url,
0010: title: "The Feed Name"
0011: });
0012: });
0013: this.component.trigger('uiNeedsFeedInfo', { feedUrl: FEED_URL });
0014: expect(eventData).toHaveBeenTriggeredOn(document);
0015: expect(eventData.mostRecentCall.data).toEqual({
0016: feedUrl: FEED_URL,
0017: title: "The Feed Name"
0018: });
0019: });
0020: });

This one test represents the single responsibility of our data component - take a uiNeedsFeedInfo event and emit a dataFeedInfo event when the request is fulfilled. Though the method a data component uses to procure the data will vary significantly depending on the source, few such components will need to respond to or emit more than one event. Our component implementation will be fairly straightforward as well; we'll bind the event callback in our initialize method, delegate the fetching to a separate (and therefore testable) executeRequest method, and invoke a callback to emit the dataFeedInfo event when the request has finished. We'll interface with remote services after we get the core functionality implemented.

0001:    define(function(require) {
0002: var defineComponent = require('flight/lib/component');
0003: function FeedService() {
0004: this.fetchFeedInfo = function(event, feed) {
0005: this.executeRequest(feed.feedUrl, this.requestCallback.bind(this));
0006: };
0007: this.executeRequest = function(feedUrl, callback) {
0008: // empty for now
0009: };
0010: this.requestCallback = function(feed) {
0011: this.trigger('dataFeedInfo', feed);
0012: };
0013: this.after('initialize', function() {
0014: this.on('uiNeedsFeedInfo', this.fetchFeedInfo);
0015: });
0016: }
0017: return defineComponent(FeedService);
0018: });

Our data component illustrates another advantage of Flight - since components are often extremely focused and concerned with only one small piece of functionality, their implementation is often very terse, and therefore very easy to read. Each method has exactly one responsibility; the component is only activated by one event; and it emits only one event when its purpose has been fulfilled. Though not all data components will be as short as our FeedService, we should strive for such simplicity. Our tests are passing, so lets commit and finish out the component with live data!

Interfacing with Remote Services

Our application has functioned solely by receiving user input and taking actions to resolve its state in response. We will now introduce a remote service to supply the data we need to have a fully functioning app. Since we can't directly access RSS feed data via AJAX due to the Same Origin Policy, we will need a separate service to fetch our feed data. Though we could write our own server-side script and access that via AJAX, we will instead use the Google Feed API. It uses JSONP (JavaScript Object Notation with Padding) to fetch remote data without being bound by the Same Origin Policy, and does not require us to have access to a server to execute a proxy script. The Google Feed API is simple enough; just call new google.feeds.Feed(url).load(callback) and a JSON-formatted version of the feed data is passed to the callback function when the data loads. We'll first test our executeRequest method to make sure it tries to invoke the Google API correctly.

0001:    it("should call the Google Feed API with the feed URL", function() {
0002: // need a mock object for Google. We don't want to ping
0003: // the server for our unit tests!
0004: var feedSpy = jasmine.createSpy();
0005: var loadSpy = jasmine.createSpy();
0006: feedSpy.prototype.load = loadSpy;
0007: // make it available globally under the right namespace
0008: window.google = { feeds: { Feed: feedSpy } };
0009: // trigger the event to start the process
0010: this.component.trigger('uiNeedsFeedInfo', { feedUrl: FEED_URL });
0011: expect(window.google.feeds.Feed.mostRecentCall.object instanceof feedSpy).
0012:toBeTruthy();
0013: expect(window.google.feeds.Feed).toHaveBeenCalledWith(FEED_URL);
0014: expect(loadSpy).toHaveBeenCalled();
0015: expect(typeof loadSpy.mostRecentCall.args[0]).toBe('function');
0016: });
0017:

The one-liner straight from the Google Feed API documentation should be enough to get our test to pass. Keeping with the simplicity of the rest of the component:

0001:    this.executeRequest = function(feedUrl, callback) {
0002: new google.feeds.Feed(feedUrl).load(callback);
0003: };

Short, sweet, and green. Now we just need to make sure our component is properly extracting the data from the response object. The documentation states that the response format contains a feed property with the data we're interested in, which will require us to extract that to emit as data with our event. Because our data access layer is fully encapsulated within this component, we should only need to update our requestCallback function to handle the change in response format (and the previous test that faked a response). Our FeedManager component, also a listener for the dataFeedInfo event, should be none the wiser for this change.

0001:    it("should emit the 'dataFeedInfo' event with the feed data as the data argument", 
0002:function() {
0003: // we don't need to inspect our mocks for this test
0004: function Feed(url) {
0005: this.url = url;
0006: }
0007: Feed.prototype.load = function(callback) {
0008: // provide some data that looks like the response format of the
0009:Google Feed API
0010: callback({
0011: feed: {
0012: feedUrl: this.url,
0013: title: "Dummy Title"
0014: }
0015: });
0016: };
0017: // make it available globally under the right namespace
0018: window.google = { feeds: { Feed: Feed } };
0019: var eventSpy = spyOnEvent(document, 'dataFeedInfo');
0020: // trigger the event to start the process
0021: this.component.trigger('uiNeedsFeedInfo', { feedUrl: FEED_URL });
0022: expect(eventSpy).toHaveBeenTriggeredOn(document);
0023: expect(eventSpy.mostRecentCall.data).toEqual({
0024: feedUrl: FEED_URL,
0025: title: "Dummy Title"
0026: });
0027: });

These sorts of tests treat the component like a "black box," which de-emphasizes how the data is retrieved, in favor of testing the request/response interface for the component. By testing the interface and not the internals, we gain some confidence that this component will behave properly in the presence of other components on the page. We just need to make a small tweak to requestCallback to get our test to pass:

0001:    this.requestCallback = function(feed) {
0002: this.trigger('dataFeedInfo', feed.feed);
0003: };

Our data component is now complete, and properly interfaces with the Google Feed API to supply our application with RSS feed data. If you load up the application at this point, you should be able to enter an RSS endpoint (e.g. http://xkcd.com/rss.xml) and see the application fill in the title!


Conclusion

We've now seen the two flavors of components that make up a Flight application. Our UI component deals strictly with user interaction, and our data component encapsulates our remote data fetching behavior by listening for application events emitted by UI components and responding with their own events containing the data. By encapsulating the data interface in this way, we allow UI components to be completely decoupled from the services that provide the data they need. There is another benefit to this approach, of course; by communicating data through our app using events, many components can listen for the data events and update themselves when new data becomes available, without having to manage the inter-component dependencies manually. In the next chapter, we will write a new UI component to display the entries for all of our feeds and demonstrate how to keep small components cleanly separated from each other. We'll also see how different components with different data interests can store relevant state information.

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

You should refresh this page.