Your First JavaScript App – V

Read the first part of this series.

The first thing we will do now, is to create the view of our application. We will first create the form, then we will add some dummy data so that we can create our list as well.

I have created a utility function here to create an HTML element and assign attributes to it so that I wouldn’t have to repeat the syntax.

// scripts/modules/contacts/contactsViewTemplates.js
var ContactsViewTemplates = (function () {
  function Cvt () {};
  /**
   * @description function creates an element with given tagName and
   * options.
   */
  Cvt.prototype.getElement = function (tagName, options, text) {
    var el = document.createElement (tagName);
    Object.keys (options).forEach (item => {
      el.setAttribute (item, options[item]);
    });

    if (text) {
      var textNode = document.createTextNode(text);
      el.appendChild(textNode);
    }

    return el;
  }
  return Cvt;
})();

And in the view, we will create an instance of this object and assign it to some local variable.

In the view, I am creating a function that should create the layout of the application. As discussed earlier, we have three main components, a form, a  view of a contact, and a container to render the list of contacts. So we need to divide our view into two parts,

  1. A form container.
  2. A list container.

We will add this process of creating the components in a function, your code should look like this:

// scripts/modules/contacts/contactsView.js
var ContactsView = (function () {
  var template = new ContactsViewTemplates ();
  function CView () {
    this.formContainer = null;
    this.listContainer = null;
  }
  /**
   * @description function to create the layout of the working area
   */
  CView.prototype.renderLayout = function (rootNode) {
    this.formContainer = template.getElement ('div', {
      'class': 'form-container'
    });

    this.listContainer = template.getElement ('div', {
      'class': 'list-container'
    });
    rootNode.appendChild (this.formContainer);
    rootNode.appendChild (this.listContainer);
  }
  return CView;
}) ();

Rendering layout will be first thing that we have to do, so in the controller, we will create another function called init and call it inside the controller constructor. We will put everything we need to run at the startup in this function.

// scripts/modules/contacts/contactsController.js
var ContactsController = (function () {
  function CCtrl (model, view) {
    this.model = model;
    this.view = view;
    this.rootNode = document.getElementById ('root');

    init.call (this);
  }

  function init () {
    this.view.renderLayout (this.rootNode);
  }


  return CCtrl;
}) ();

Note: The reason for doing init.call (this) is, we want to execute the init function with this which will have the context of the created object from this constructor.

The contact form

We will create the contact form in one go, it is a simple task. As the function to create the form needs not to be accessed by the controller, we can create a local function in the view instead of putting it on the prototype.

The following function creates the form with 4 input elements and two buttons, one to add the items, and second to clear the form.

/**
   * @description function to create the contact form.
   * @returns {HTMLElement}
   */
  function getContactForm () {
    var formBody = template.getElement ('form', {
      'class': 'form-body',
      'id': 'contactForm',
    });

    var firstName = template.getElement('input', {
      'placeholder': 'Enter first name',
      'type': 'text',
      'class': 'form-input',
      'name': 'firstName',
      'id': 'firstName'
    });
    var lastName = template.getElement('input', {
      'placeholder': 'Enter last name',
      'type': 'text',
      'class': 'form-input',
      'name': 'lastName',
      'id': 'lastName'
    });
    var mobile = template.getElement('input', {
      'placeholder': 'Enter mobile number',
      'type': 'phone',
      'class': 'form-input',
      'name': 'mobile',
      'id': 'mobile'
    });
    var email = template.getElement('input', {
      'placeholder': 'Enter e-mail id',
      'type': 'text',
      'class': 'form-input',
      'name': 'email',
      'id': 'email'
    });

    var submitButton = template.getElement('button', {
      'type': 'submit',
      'id': 'add'
    }, 'Add');

    var clearButton = template.getElement('button', {
      'type': 'reset',
      'id': 'clear'
    }, 'Clear');

    // append all fields to the form.
    formBody.appendChild(firstName);
    formBody.appendChild(lastName);
    formBody.appendChild(mobile);
    formBody.appendChild(email);
    formBody.appendChild(submitButton);
    formBody.appendChild(clearButton);

    // at the end, return form body
    return formBody;
  }

Once this function is created, we can call this function inside renderLayout function [last line].

    .
    .
    .
    rootNode.appendChild (this.formContainer);
    rootNode.appendChild (this.listContainer);

    rootNode.appendChild (getContactForm());
    .
    .
    .

Adding a contact

When the user clicks on the button ‘Add’, this is what we need to do,

  1. Collect data from user input.
  2. Add it to the available list
  3. Update the view/list to reflect newly added data.

In a real application, the storage and sending data to the server to store in the database is done by the API provided to the client side [front-end JavaScript]. But we will not discuss how that is done, instead, we will use the localStorage API available in HTML5 to store the data locally. If you do not know what localStorage is, you should not worry about that not. You can still go ahead and consider the storage to be a black-box and work only on front-end APIs.

Create the Mock Data

For the purpose of easy understanding, let us create a mock data [hard-coded values] which we would want to display on the page. We will reuse this logic to update the list later.

Since we already know that the data comes from the model and the controller communicates with the model and view to render the data. Let us create some mock data in the model.

// scripts/modules/contacts/contactsModel.js
var ContactsModel = (function () {
  function CModel () {
    this.mockData = [
      {
        firstName: 'John',
        lastName: 'Smith',
        mobile: '9999999999',
        email: 'john@example.com',
      },
      {
        firstName: 'Jane',
        lastName: 'Smith',
        mobile: '9999999999',
        email: 'jane@example.com',
      },
    ]
  }


  // shared function to return mock data
  CModel.prototype.getMockData = function() {
    return this.mockData;
  }

  return CModel;
}) ();

Display the Contact List

We have the mock data, we have the container where we want to render the list, all we need to do now is to code the controller such that it gets the data from the model and displays on the screen.

Plus, this should happen during [just after] the initialisation, because when the application loads we would want to render all available contact list.

So now we know, we need a function to render the list, and we need to call this function during initialisation. So let’s move to the controller file.

Here are the steps to do this,

  1. The view should provide a function to render the list (view).
  2. The view should get the data which it needs to inject in the list, for example, the value of firstName, lastName, and so on.
  3. This function needs to be called inside the init function of the controller.

So the exact code will look like this,

// scripts/modules/contacts/contactsController.js
function init () {
    this.view.renderLayout (this.rootNode);

    // render the list of contact
    this.view.renderList (this.model.getMockData());
}

Note: renderList is a function available in the view [we have not written it yet], and getMockData is a function available in the model.

Let’s go back to the view and create the renderList function.

  1. renderList function accepts an array of objects.
  2. Iterate the received list, and for each item in the array, create a row which renders the contact data.
  3. To create an individual row, we’ll create another function, renderRow, this will accept a contact object. 
// scripts/modules/contacts/contactsView.js

  // function to create list of items.
  CView.prototype.renderList = function (list) {
    // for each item of the list, render the row.
    for (var index = 0; index < list.length; index++) {
      renderRow (this.listContainer, list[index]);
    }
  }

  function renderRow (listContainer, item) {
    var row = template.getElement('div', {
      'class': 'row-item',
    });

    // now, iterate over the object and create divs and then append to row.
    for (var key in item) {
      var val = item[key];
      var span = template.getElement('span', {
        'class': 'col',
      }, val);
      row.appendChild(span);
    }

    // in the end, append row to the listContainer.
    listContainer.appendChild(row);
  }

At this point, the page will not look great, but it renders the data that we had created in the model (mock data), and that’s what we need now.

If you noticed, the renderRow function is a private function, so we cannot call this function when we would need to add the new row. To fix this, 

  1. we can create the renderRow function on the prototype to make it available on the object instance.
  2. or, we can create another function on the prototype and then call the render function from this function.

This is a design choice, i.e. how do you want to implement it. The benefit of the second method is, the renderRow function will remain private and the other part of the application will not know about the actual implementation. Though in this case, the only critical thing that renderRow function is doing, it is rendering the data on the screen. Since we have already created the first type of function, let’s create the second type and add a function in the prototype which will call the private renderRow function.

// scripts/modules/contacts/contactsView.js
CView.prototype.createRow = function (item) {
    renderRow(item);
}

As expected, createRow function will need an object [the contact] to pass it to the renderRow function.

In the next post, we will add the user interaction in the contact form. Initially, we will add the new data in the mock data, i.e. update the mock data. In another post, we will implement the localStorage, so if you are not sure how does it work, you work on it later as a separate topic.