Swapping Views In The DOM - MANAGING THE DOM - Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

Building Backbone Plugins: Eliminate The Boilerplate In Backbone.js Apps (2014)

PART 2: MANAGING THE DOM

Chapter 8: Swapping Views In The DOM

Most small Backbone applications can get away with very small, simple methods of managing the DOM. Backbone.View implementations typically contain the majority of what is needed, so why not use them exclusively? As an application grows in size and complexity, though, a more modular application design become necessary. Different areas of functionality within an application should not be directly coupled to other, unrelated areas of functionality. When an application begins to move down this path, it becomes more difficult for Backbone.View to be the sole manager of the DOM content. Having hard coded references to page level DOM elements becomes unmaintainable. Every place in the application that has the DOM element reference is yet another place that has to be changed, if / when the DOM is changed. Fortunately, it is easy to create an object that can manage a DOM element and it’s displayed Backbone.View.

A Need To Change Out A View

I was working on a location management application for a client at one point. It started out with three distinct regions on the screen:

1. Navigation (a tree view)

2. Main content

3. An add/edit form

Once this was in place a new requirement came along: a complex search with search results. To implement this, I needed to modify the application’s user interface to swap out the grid and add/edit form out and put in a search results screen instead. The idea being that when the user does a search, the main content area will show the search results. The user can then go back to the location management aspect of the app whenever they need to, as well.

After a bit of searching and experimenting, I found a high level pattern that made this easy. I also realized that I had previously worked with, and implemented the core of this pattern without knowing it.

Microsoft Prism: Regions

Several years ago, Microsoft released a framework for it’s WPF and Silverlight runtimes, called Prism. This was a large scale composite application framework that people used to build well structured and decoupled apps in XAML. I never had a chance to use this framework directly, but I worked with a team of developers that did use it.

One of the things that I liked about what I saw in Prism was the way it used the idea of “regions” to compose the user interface. The gist of it is that you could define a visible area of the screen and build a layout for it without knowing what content was going to be displayed in it at runtime. Then at runtime, the application modules could register themselves to have content displayed in the various regions of the screen.

This pattern fits perfectly with the direction that my Backbone app were heading, so I decided to borrow the names and build my own version in JavaScript.

A Simple Region For Backbone

In Prism, a region is defined in the XAML markup. In web applications, we have HTML. Similarly, in XAML a region manager is code that you write in C# or other .NET languages. In a web app, though, a region manager is going to be JavaScript. Backbone.js provides a good separation between the markup and the code to run that markup through it’s Views, so I initially thought about going down this path for my regions. After a bit of thinking, though, I realized that I didn’t necessarily need a Backbone view. What I really need is a JavaScript object that do the following:

· Represent an existing DOM node

· Change out the contents of that DOM node

· Call any required rendering and initialization for content views that will be displayed

· Call any required cleanup for content views when they are removed

What I came up with as an initial pass at handling these needs, was the following (hard coded specifically to use a “#mainregion” element from the DOM):

1 // Define a "Region" object that is hard coded

2 // to manage the "#mainregion" DOM element

3 Region = (function (Backbone, $) {

4 var el = "#mainregion";

5 var currentView;

6 var region = {};

7

8 // A method to close the current view

9 var closeView = function (view) {

10 if (view && view.remove) {

11 view.remove();

12 }

13 };

14

15 // A method to render and show a new view

16 var openView = function (view) {

17 view.render();

18 $(el).html(view.el);

19 };

20

21 // Export the API to show a view and

22 // close an existing view, if one is

23 // already in this DOM element

24 region.show = function (view) {

25 closeView(currentView);

26 currentView = view;

27 openView(currentView);

28 };

29

30 return region;

31 })(Backbone, jQuery);

Having this Region object allowed me to swap out the contents of the #mainregion DOM element without any of my views having to know about the DOM element that they were being displayed within. It also prevented the views from having to know about each other. There is now an intermediate object that knows about the view it is currently displaying, and can remove it when a new view needs to be displayed.

Extracting A Reusable Region Type

From the initial Region object, it was easy to extract a re-usable type that can be instantiated and have an el assigned to it as the element to manage.

1 // Define a re-usable "Region" object

2 var Region = (function (Backbone, $) {

3

4 // Define the Region constructor function

5 // accept an object parameter with an `el`

6 // to define the element to manage

7 function R(options){

8 this.el = options.el;

9 this.currentView = undefined;

10 }

11

12 // extend the Region with the correct methods

13 _.extend(R.prototype, {

14

15 // A method to close the current view

16 closeView: function (view) {

17 if (view && view.remove) {

18 view.remove();

19 }

20 },

21

22 // A method to render and show a new view

23 openView: function (view) {

24 this.ensureEl();

25 view.render();

26 this.$el.html(view.el);

27 },

28

29 // ensure the element is available the

30 // first time it is used. cache it after that

31 ensureEl: function(){

32 if (this.$el){ return; }

33 this.$el = $(this.el);

34 },

35

36 // show a view and close an existing view,

37 // if one is already in this DOM element

38 show: function (view) {

39 this.closeView(this.currentView);

40 this.currentView = view;

41 this.openView(view);

42 }

43 });

44

45 // export the Region type so it can be used

46 return R;

47 })(Backbone, jQuery);

The Region can be used by creating instances with the new keyword, passing an object literal with an el specified:

1 var mainRegion = new Region({

2 el: "#mainregion"

3 });

Once I had this in place and started working with it more, I realized that there were a few other things that the Region could do for me.

Handing onShow Calls

In Chapter 3, I showed how to handle jQuery plugins that rely on the DOM, using an onShow method to know that the view is in the DOM. With the Region object in control of placing a View in the DOM, it makes sense for it to also be responsible for calling the onShow method.

1 // show a view and close an existing view,

2 // if one is already in this DOM element

3 show: function (view) {

4 this.closeView(this.currentView);

5 this.currentView = view;

6 this.openView(view);

7

8 // run the onShow method if it is found

9 if (_.isFunction(view.onShow)){

10 view.onShow();

11 }

12 }

Adding this to the Region was easy and made the region that much more valuable. Now any time I need to work with a DOM-dependent plugin, I can add an onShow method and if I’m displaying the view with a Region object, it will be called at the appropriate time.

Lessons Learned

Building objects that encapsulate common processes can reduce the amount of code needed and provide opportunity to consolidate features in many cases. Looking outside of JavaScript and Backbone can provide insight and opportunity to solve problems in common ways, as well.

Beg, Borrow and Steal

JavaScript isn’t doing anything new with scalable, stateful application development. The vast majority of the patterns and practices that are being used to build Single Page Apps have been around for 20 or 30 years, in fact. It is important to understand the history of stateful application development, not just to avoid repeating the mistakes of the past. Understanding the past and even the current state and future of other languages and platforms allows us to properly beg, borrow and steal patterns and concepts.

In this case, I decided to borrow an idea from Microsoft’s Prism framework when it came time to manage swapping view instances in and out of the DOM. Having an existing codebase, documentation, example apps and other resources available made it easy for me to see how this worked and why. I was able to adapt the core concepts from a C#/XAML/Windows world in to the JavaScript world, with great success.

DRYing Up DOM References

The “Don’t Repeat Yourself” principle says that we should reduce duplication in code. This makes life as a developer easier by reducing the number of place that I have to change something. DOM references are no different in this regard - and may even be subject to more frequent change, creating a greater need to reduce the number of references to a given DOM element.

By introducing a Region object to manage the content of a specific DOM element, I was able to reduce the number of references to the “#mainregion” in my application. When (and I truly mean “when” - it happened, multiple times) the DOM element in question changed and I needed to update the selector that my application used, I only had to do it in one location instead throughout every file that needed to display content in that element.

Consolidating Needs

The Region object initially provided a way to manage the contents of a DOM element, but it quickly became apparent that it could do more than that. Since this was the object that knew precisely when a Backbone.View was being inserted in to the DOM, it made sense to have the Region check for and call the onShow method of a View instance. Similarly, the need to manage View cleanup became the responsibility of the Region because the Region was already handling showing and removing the views. It was a natural progression for the close and/or remove method of a Backbone.View to be called from the Region, when it was closing a view.

Make Assumptions, But Provide Fallbacks

Building a plugin or abstraction that takes advantage of another is usually very beneficial. Assumptions can be made about how certain objects will behave, allowing the combination of existing ideas building new ideas faster. But if the Backbone way of providing a flexible library where pieces can be used independently is to be maintained, fallback mechanisms should be provided when possible.

In the case of a Region, I can facilitate DOM dependent jQuery plugins by having the region call an onShow method of a view. Not every view is going to need this method, though, so I check to ensure the method is available before calling it.

Similarly, I can make assumptions about a close method being available on my view if I am using Regions and my own View layer. But if I want other developers to be able to take advantage of Regions without forcing them to use my View layer, I need to provide a fallback. Backbone.View has a built-in remove method and is a suitable fallback in this case. If a view instance does have a close method, I call that. If not, I call the remove method instead.