Basic View coding rules

Now, it is time to start coding the first View component. To help us 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 at the NewInvestmentView.js file, it uses jQuery to get the element for this selector and to store it in an instance variable $element (source), as follows:

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

To make sure this code works, we should write the following test for it in the NewInvestmentViewSpec.js file:

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

The toExist matcher is a custom matcher provided by the Jasmine jQuery extension to check whether an element exists in the document. It validates the existence of the property on the JavaScript object and also the successful association with the DOM element.

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

Another advantage of having an 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, as follows:

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

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

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

However, 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, as follows:

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

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

Since we will use this pattern everywhere inside the View code, we can create a function and use it instead, as shown in the following code:

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

Now let's 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 this:

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

However, 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 NewInvestmentView.

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

To fix this, the developer would need to look at the entire code base for references to that class name.

A much safer approach is to respect the View and use its APIs, as shown in the following code:

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

When implemented, that would look like the following:

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.

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

Integrating 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 about integrating NewInvestmentView and InvestmentListView?

You could write an acceptance criterion for NewInvestmentView, as follows:

Given the new investment View, when its add button is clicked, then it should add an investment to the list of investments.

This is 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, as follows:

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 InvestmentListView as its listView parameter.

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

function NewInvestmentView (params) {
  this.listView = params.listView;
  
  this.$element.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:

Integrating Views with observers

This shows a 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.

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, we want to list investments in a table too. This would impose a change in NewInvestmentView to support both the list and table Views, as follows:

function NewInvestmentView (params) {
  this.listView = params.listView;
  this.tableView = params.tableView;
  
  this.$element.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, future-proof solution. Let's rewrite it as:

Given the Investment Tracker application, when a new investment is created, then it should add the investment to the list of investments.

We can see by the acceptance criterion that it has introduced a new subject to be tested: 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, as shown in the following code:

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);
    });
  });
});

We can see the same setup code that once was inside the NewInvestmentView spec. It loads the fixtures required by both Views, instantiates both 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, we can see the function call to the newView.create function to create a new investment.

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

But how does the integration happen? We can see that 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 newView. This observer function will be invoked later when a new investment is created.

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

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 even be removed in the future.

Later, when the create method is invoked, it invokes _callback, passing the new investment as a parameter, as follows:

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

A more complete implementation would need to allow multiple calls to onCreate, storing every passed callback.

Here is the solution illustrated for better understanding:

Integrating Views with observers

Using callbacks to integrate the two Views

Later, in Chapter 7, Testing React.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