Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)
PART 4: APPLICATION INFRASTRUCTURE
Chapter 17: Building With Components
There are a lot of different definitions of “component” floating around these days. For the purposes of this book, a generalized definition will be borrowed from the up-coming W3C standard for Web Components, and from frameworks such as AngularJS which provide a way to build component based applications.
Components are a combination of visual portions of the application and the logic and processing of that visualization. They encapsulate a small feature or sub-set of a feature in a manner that allows the component to be wired together with other components, creating a larger feature set or functional area of an application. They have at least one view with some amount of logic and process (a.k.a. “workflow�).
|
Web Components: For more information on Web Components, see this work-in-progress document from the W3C and the Polymer project. AngularJS is also a current framework that builds on the ideas of component based architecture and can be found at AngularJS.org. |
Defining A Component Constructor
Defining a Component starts like most other objects - with a constructor function. Components have a visual element to them, though, so you will need a way to manage where the component is displayed. You could pass a jQuery selector in to the constructor function, and possibly duplicate a lot of code that you wrote in Chapter 6. Or, use the Region object from that chapter and allow a region instance to be passed in to the component’s constructor.
1 var Component = function(options){
2 this.options = options || {};
3 this.region = this.options.region;
4 };
Now when you create a Component, it will look for a region option in the constructor’s object literal parameter. But a Component is meant to be heavily customized with the behavior and needs of the functionality encapsulated. To do that, you’ll want to allow more than just simple Component instances.
An .extend-able Component
You can borrow several of the techniques that Backbone provides when creating a Component definition. This includes both the use of a .extend method, to allow for object extension, and an initialize method to allow for a custom constructor function in the Component. It may be useful to allow a Component’s region to be specified with the object definition, as well.
1 var Component = function(options){
2 this.options = options || {};
3
4 // pull the region from the definition, or the
5 // constructor options. precedence goes to the
6 // constructor options as an override
7 this.region = this.options.region || this.region;
8
9 // execute an `initialize` function if it's there
10 if (_.isFunction(this.initialize)){
11 this.initialize(this.options);
12 }
13 });
14
15 // Borrow Backbone's `extend` method
16 Component.extend = Backbone.Model.extend;
The line that retrieves the region checks to see if one was provided in the options, first. If one was, it uses that. If none was provided in the options, a region on an object definition is used (if provided). That way, if you provide a region on a Component definition, you can still override it in the constructor of a specific instance.
The check for the initialize function ensures that it is a function, if it exists at all. If it does, it calls the initialize function with the options passed through the constructor. By using this.options as the argument, you can ensure the function at least receives an empty object literal. This will help prevent errors and other problems when defining components with an initialize function, later.
Lastly, the extend method from Backbone.Model is attached to the Component function. It doesn’t matter which Backbone object you pull this function from - it’s the same function on all of them. But Backbone does not directly expose this function, so you’ll have to pull it from one of the Backbone objects directly.
Handling Events Within The Component
With Backbone being an event-driven library, and the majority of objects used in Backbone apps able to trigger events, it makes sense for the Component to be able to respond to and handle events. Mixing in Backbone.Events on the Component’s prototype will generally take care of this. But it would also be nice to have the Component clean up any event handler it can, when closing the component.
1 _.extend(Component.prototype, Backbone.Events, {
2
3 // allow a Component to be closed
4 close: function(){
5 // unhook any `listenTo` events
6 this.stopListening();
7
8 // close the region
9 if (this.region){
10 this.region.close();
11 }
12
13 // call an `onClose` method for custom cleanup
14 if (_.isFunction(this.onClose)){
15 this.onClose();
16 }
17 }
18 });
Providing a close method allows any Component instance to be closed. It also gives the Component an opportunity to remove any bound event handlers that were set up with the listenTo method of the Component. It also closes the region that it holds on to, if any (this forces the view displayed in the region to close, as well). Lastly, it calls an onClose method if one exists - just like the custom View types from the first part of this book.
|
Consistent Is Clean Consistency is an important aspect in API design. Providing the same method names with the same semantics across multiple objects creates a level of consistency and predictability. It also gives developers an easy way to understand the expected uses of the API. All of this leads toward clean use of the API and better code in general. For more information on good API design, see Brandon Satrom’s article on Secrets of Awesome JavaScript API Design. |
As you can see, there isn’t too much behind the Component type. In this case, the idea is more about the semantics and use case of the object type, than the details of the type implementation. Of course there will be additional bits of functionality that can be encapsulated in the core Component type, over time. But other pieces can be added as needed.
With the core of a Component defined, you can begin to build the application specific components.
Building A FilteredList Component
In chapter 13, you built a collection filtering object that can return a list of items filtered out of a Backbone.Collection instance. But having that object is only half the story. You also need a way for users to provide input for the filter, typically in a search form or other “filter” input box. To do this, then, a filtering component will be built on top of the filtered collection object.
The Basic Component Options
Define a component that takes three options from the initialize function: a view type (to display the collection) and the collection instance to be displayed will come form the .options, and a region in which to display the view is already handled by the base Component.
1 var FilteredList = Component.extend({
2
3 initialize: function(options){
4 this.viewType = options.viewType;
5 this.collection = options.collection;
6 }
7
8 });
Allowing these three parameters to be passed in to the component means you can change out the view that is used for display, the collection being displayed, and where in the app it is displayed, without having to modify the component implementation.
Render The Collection, Display The View
Add a show method to the component. This method will set up the filtered collection, pass it to an instance of the view type, render the view and finally, show the view in the region.
1 var FilteredList = Component.extend({
2
3 // ... initialize method is here
4
5 show: function(){
6 // set up the filtered collection
7 this.filteredCollection = new Backbone.Collection();
8
9 // create an instance of the view,
10 // with the filtered collection to display
11 this.view = new this.viewType({
12 collection: this.filteredCollection
13 });
14
15 // show the view in the region
16 this.region.show(this.view);
17 }
18
19 });
Filtering From User Input
The view needs to pass the filter information from the DOM out to the component that is controlling the view. There are a number of options for doing this, but the easiest is through the use of an event. Have the view trigger a “filter:updated” event from the DOM click handler for the #runbutton. In this event, include the filter information from the view.
1 var FilteredView = BBPlug.CollectionView.extend({
2 // ... existing stuff...
3
4 events: {
5 "click #run": "runClicked"
6 },
7
8 runClicked: function(e){
9 e.preventDefault();
10
11 var filterInfo = {
12 // get the info from the view inputs
13 };
14
15 this.trigger("filter:updated", filterInfo);
16 }
17 });
Now the component needs to respond to this event, and run the filtering process against the original collection, passing the results to the filtered collection. The best place to set up the handler is in the show method, but you will want to create a separate method for doing the actual filtering - one that can be called from any part of the app that has a reference to the component.
1 var FilteredList = Component.extend({
2 // ...
3
4 show: function(){
5 // ...
6
7 this.listenTo(this.view, "filter:updated, this.filter);
8 },
9
10 filter: function(filterOptions){
11 // handle filtering in whatever manner is appropriate
12 var results = this.collection.where(filterOptions);
13 // populate the filtered list
14 this.filteredCollection.reset(results);
15 }
16 });
The filter method can be called any time the data needs to be filtered and displayed in the view. This can be from the view instance triggering a “filter:updated” event, or from any object that has a reference to the component instance. With this flexibility, the view is not required to trigger this event, either. You could create a view that does not have the input for creating the filter options. In that case, another object outside of the component would be responsible for setting up the filter options and calling the filter method.
Using The FilteredList Component
With everything in place for both the FilteredList component, and the updated FilteredView, you can add it to your application.
1 var myCollection = new Backbone.Collection([
2 {a: "first", b: "second"},
3 {a: "third", b: "second"},
4 {a: "first", b: "fourth"},
5 {a: "second", b: "third"}
6 ]);
7
8 var myFilterList = new FilteredList({
9 viewType: FilteredView,
10 collection: myCollection,
11 region: someRegion
12 });
13
14 myFilterList.show();
You now have a complete component that is displayed on the screen in the region specified. It can filter the collection provided and update the displayed items based on the filters. You can also call the .filter method of the component from any code that has a reference to the component instance. And all of this is encapsulated in only a few lines of code where it is being used.
Lessons Learned
Components are the way of the future in web development. The more we move toward a component based architecture, the more composable our systems become.
Reduced Cost
Composition provides significantly lower costs when putting systems together and modifying systems. It allows us to plug in and remove pieces as needed.
Build UI On Top Of Process
Building a component to place in to an application does not require 100% custom / unique code. It is common and recommended that you look at existing processes and visual aspects of your system, and build reusable components on top of those processes.
Purpose Trumps Implementation
Like many design patterns, we are often faced with code that looks very similar on the surface. The proxy pattern and decorator pattern, for example, are often interchangeable in implementations. The primary differentiator being intention and semantics. The core implementation of a Component is another case where the code shares many details with other pieces that you have been working with. This is not a bad thing. In fact, this is favorable as it creates familiar patterns of use in your applications.
It is important, however, to understand the semantics and differences between what we are calling a Components and what we are calling a View. Just because they look the same in implementation, doesn’t mean they serve the same purpose.