Basic View coding rules

Now it is time to start coding the first View component. To help you through the process we are going to lay two basic rules for View coding happiness:

  • The View should encapsulate a DOM element
  • Integrate Views with observers

So let's see how they work individually.

The View should encapsulate a DOM element

As mentioned earlier, a View is the behavior associated with a DOM element, so it makes sense to have this element related to the View. A good pattern is to pass a CSS selector in the View instantiation that indicates the element to which it should refer. Here is the spec for the NewInvestmentView component:

describe("NewInvestmentView", function() {
  var view;
  beforeEach(function() {
    loadFixtures('NewInvestmentView.html'),
    view = new NewInvestmentView({selector: '#new-investment'});
  });
});

In the constructor function, it uses jQuery to get the element for this selector, and store it in an instance variable $el (source):

function NewInvestmentView (params) {
  this.$el = $(params.selector);
}

To make sure this code works, we should write a test for it:

it("should expose a property with its DOM element", function() {
  expect(view.$el).toExist();
});

The toExist is a custom matcher provided by the Jasmine jQuery extension to check if an element exists in the document. It not only validates the existence of the property on the JavaScript object, but also that the association with the DOM element worked.

Passing the selector to the View allows it to be instantiated multiple times to different elements on the document.

Another advantage of having the explicit association is knowing that this View is not changing anything else on the document, as we will see next.

A View is the behavior associated with a DOM element, so it shouldn't be messing around everywhere on the page. It should only change or access the element associated with it.

To demonstrate this concept, let's implement another acceptance criterion regarding the default state of the View.

it("should have an empty stock symbol", function() {
  expect(view.getSymbolInput()).toHaveValue(''),
});

A naive implementation of the getSymbol method, might use a global jQuery lookup to find the input and return its value:

NewInvestmentView.prototype = {
  getSymbolInput: function () {
    return $('.new-investment-stock-symbol')
  }
};

But that could lead to a problem; if there is another input with that class name somewhere else in the document, it might get the wrong result.

A better approach is to use the View's associated element to perform a scoped lookup:

getSymbolInput: function () {
  return $this.$el.find('.new-investment-stock-symbol')
}

The find function will only look for elements that are children of this.$el. It is as if this.$el represents the entire document for the View.

Since we will be using this pattern everywhere inside the View code, we can create a function and use it instead:

NewInvestmentView.prototype = {
  $: function () {
    return this.$el.find.apply(this.$el, arguments);
  },
  getSymbolInput: function () {
    return this.$('.new-investment-stock-symbol')
  }
};

Now suppose that from somewhere else in the application, we want to change the value of a NewInvestmentView form input. We know its class name, so it could be as simple as:

$('.new-investment-stock-symbol').val('from outside the view'),

But that simplicity hides a serious problem of encapsulation. This one line of code is creating a coupling with what should be an implementation detail of the NewInvestmentView.

If tomorrow, another developer changes the NewInvestmentView, renaming the input class name from .new-investment-stock-symbol to .new-investment-symbol, that one line would be broken.

To fix it, the developer would need to look at the entire codebase for references to that class name.

A much safer approach is to respect the View and use its APIs:

newInvestmentView.setSymbol('from outside the view'),

Which when implemented would look like:

NewInvestmentView.prototype.setSymbol = function(value) {
  this.$('.new-investment-stock-symbol').val(value);
};

That way, when the code gets refactored, there is only one point to perform the change—inside the NewInvestmentView implementation.

And since there is no sandboxing in the browser's document, which means that from anywhere in the JavaScript code you can make a change anywhere in the document, there is not much you can do, besides good practice, to prevent these mistakes.

Integrate Views with observers

Following the development of the Investment Tracker application, we would eventually need to implement the list of investments. But how would you go on integrating the NewInvestmentView and the InvestmentListView?

You could write an acceptance criterion for the NewInvestmentView as: Given the new investment View, when its add button is clicked, then it should add an investment to the list of investments.

This is a very straightforward thinking, and you can see by the writing that we are creating a direct relationship between the two Views. Translating this into a spec clarifies this perception:

describe("NewInvestmentView", function() {
  beforeEach(function() {
    loadFixtures('NewInvestmentView.html'),
    appendLoadFixtures('InvestmentListView.html'),

    listView = new InvestmentListView({
      id: 'investment-list'
    });

    view = new NewInvestmentView({
      id: 'new-investment',
      listView: listView
    });
  });

  describe("when its add button is clicked", function() {
    beforeEach(function() {
      // fill form inputs
      // simulate the clicking of the button
    });

    it("should add the investment to the list", function() {
      expect(listView.count()).toEqual(1);
    });
  });
});

This solution creates a dependency between the two Views. The NewInvestmentView constructor now receives an instance of the InvestmentListView as its listView parameter.

And on its implementation, the NewInvestmentView calls the addInvestment method of the listView object when its form is submitted:

function NewInvestmentView (params) {
  this.listView = params.listView;
  
  this.$el.on('submit', function () {
    this.listView.addInvestment(/* new investment */);
  }.bind(this));
}

To better clarify how this code works, here is a diagram of how the integration is done:

Integrate Views with observers

Direct relationship between the two Views

Although very simple, this solution introduces a number of architectural problems. The first, and most obvious, is the increased complexity of the NewInvestmentView specs.

And secondly, it makes evolving these components even more difficult, due to the tight coupling.

To better clarify this last problem, imagine that in the future you want to list investments also in a table. This would impose a change in the NewInvestmentView to support both the list and table Views:

function NewInvestmentView (params) {
  this.listView = params.listView;
  this.tableView = params.tableView;
  
  this.$el.on('submit', function () {
    this.listView.addInvestment(/* new investment */);
    this.tableView.addInvestment(/* new investment */);
  }.bind(this));
}

Rethinking on the acceptance criterion, we can get into a much better and future proof solution. Let's rewrite it as: Given the Investment Tracker, when a new investment is created, then it should add the investment to the list of investments.

You can see by the acceptance criterion that it has introduced a new subject to be tested: the Investment Tracker. This implies a new source and spec file. After creating both the files accordingly and adding them to the runner, we can write this acceptance criterion as a spec:

describe("InvestmentTracker", function() {
  beforeEach(function() {
    loadFixtures('NewInvestmentView.html'),
    appendLoadFixtures('InvestmentListView.html'),

    listView = new InvestmentListView({
      id: 'investment-list'
    });

    newView = new NewInvestmentView({
      id: 'new-investment'
    });

    application = new InvestmentTracker({
      listView: listView,
      newView: newView
    });
  });

  describe("when a new investment is created", function() {
    beforeEach(function() {
      // fill form inputs
      newView.create();
    });

    it("should add the investment to the list", function() {
      expect(listView.count()).toEqual(1);
    });
  });
});

You can see the same setup code that once was inside the NewInvestmentView spec. It loads the fixtures required by both Views, instantiates both an InvestmentListView and NewInvestmentView, and creates a new instance of InvestmentTracker passing both Views as parameters.

Later on, while describing the behavior when a new investment is created, you can see the function call to the newView.create function to create a new investment.

And later it checks that a new item was added to the listView object by checking that the listView.count() is equal to 1.

But how does the integration happen? We can see it by looking at the InvestmentTracker implementation:

function InvestmentTracker (params) {
  this.listView = params.listView;
  this.newView = params.newView;

  this.newView.onCreate(function (investment) {
    this.listView.addInvestment(investment);
  }.bind(this));
}

It uses the onCreate function to register an observer function as a callback at the newView. This observer function will be invoked later when a new investment is created.

The implementation inside the NewInvestmentView is quite simple. The onCreate method stores the callback parameter as an attribute of the object:

NewInvestmentView.prototype.onCreate = function(callback) {
  this._callback = callback;
};

The naming convention of the _callback attribute might sound strange, but it is a good convention to indicate it as a private member.

Although the prepended underline character won't actually change the visibility of the attribute, it at least informs a user of this object that the _callback attribute might change or be even removed in the future.

And later when the create method is invoked, it invokes _callback passing the new investment as a parameter.

NewInvestmentView.prototype.create = function() {
  this._callback(/* new investment */);
};

Here is the solution illustrated for better understanding:

Integrate Views with observers

Using callbacks to integrate the two Views

Later in Chapter 7, Testing Backbone.js Applications, we will see how the implementation of this NewInvestmentView spec turned out to be.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset