Create MVC

Whenever your application grows beyond a single script with a few dozen lines, it gets harder and harder to manage without a good separation of roles among app components. One of the most common models for structuring a complex application, no matter what language, is the Model-View-Controller (MVC) and its variants, like Model-View-Presentation (MVP).

There are several frameworks to help apply MVC concepts to a Javascript application, and most of them, as long as they are CSP compliant, can be used in a Chrome App. In this lab, we'll add an MVC model using both pure JavaScript and the AngularJS framework. Most of the AngularJS code from this section was copied, with small changes, from the AngularJS Todo tutorial.

Note: Chrome Apps don't enforce any specific framework or programming style.

Create a simple view

Add MVC basics

If using AngularJS, download the Angular script and save it as angular.min.js.

If using JavaScript, you will need to add a very simple controller with basic MVC functionalities: JavaScript controller.js

Update view

Change the AngularJS index.html or JavaScript index.html to use a simple sample:

Angular
JavaScript
<!doctype html>
<html ng-app ng-csp>
  <head>
    <script src="angular.min.js"></script>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <ul>
        <li>
          {{todoText}}
        </li>
      </ul>
      <input type="text" ng-model="todoText"  size="30"
             placeholder="type your todo here">
    </div>
  </body>
</html>
    
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <ul>
        <li id="todoText">
        </li>
      </ul>
      <input type="text" id="newTodo" size="30"
             placeholder="type your todo here">
    </div>
    <script src="controller.js"></script>
  </body>
</html>
    

Note: The ng-csp directive tells Angular to run in a "content security mode". You don't need this directive when using Angular v1.1.0+. We've included it here so that the sample works regardless of the Angular version in use.

Add stylesheet

AngularJS todo.css and JavaScript todo.css are the same:

body {
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
}

ul {
  list-style: none;
}

button, input[type=submit] {
  background-color: #0074CC;
  background-image: linear-gradient(top, #08C, #05C);
  border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
  color: white;
}

.done-true {
  text-decoration: line-through;
  color: grey;
}

Check the results

Check the results by reloading the app: open the app, right-click and select Reload App.

Create real Todo list

The previous sample, although interesting, is not exactly useful. Let's transform it into a real Todo list, instead of a simple Todo item.

Add controller

Whether using pure JavaScript or AngularJS, the controller manages the Todo list: AngularJS controller.js or JavaScript controller.js.

Angular
JavaScript
function TodoCtrl($scope) {
  $scope.todos = [
    {text:'learn angular', done:true},
    {text:'build an angular Chrome packaged app', done:false}];

$scope.addTodo = function() {
    $scope.todos.push({text:$scope.todoText, done:false});
    $scope.todoText = '';
  };

$scope.remaining = function() {
    var count = 0;
    angular.forEach($scope.todos, function(todo) {
      count += todo.done ? 0 : 1;
    });
    return count;
  };

$scope.archive = function() {
    var oldTodos = $scope.todos;
    $scope.todos = [];
    angular.forEach(oldTodos, function(todo) {
      if (!todo.done) $scope.todos.push(todo);
    });
  };
}
    
(function(exports) {

  var nextId = 1;

  var TodoModel = function() {
    this.todos = {};
    this.listeners = [];
  }

  TodoModel.prototype.clearTodos = function() {
    this.todos = {};
    this.notifyListeners('removed');
  }

  TodoModel.prototype.archiveDone = function() {
    var oldTodos = this.todos;
    this.todos={};
    for (var id in oldTodos) {
      if ( ! oldTodos[id].isDone ) {
        this.todos[id] = oldTodos[id];
      }
    }
    this.notifyListeners('archived');
  }

  TodoModel.prototype.setTodoState = function(id, isDone) {
    if ( this.todos[id].isDone != isDone ) {
      this.todos[id].isDone = isDone;
      this.notifyListeners('stateChanged', id);
    }
  }

  TodoModel.prototype.addTodo = function(text, isDone) {
    var id = nextId++;
    this.todos[id]={'id': id, 'text': text, 'isDone': isDone};
    this.notifyListeners('added', id);
  }

  TodoModel.prototype.addListener = function(listener) {
    this.listeners.push(listener);
  }

  TodoModel.prototype.notifyListeners = function(change, param) {
    var this_ = this;
    this.listeners.forEach(function(listener) {
      listener(this_, change, param);
    });
  }

  exports.TodoModel = TodoModel;

})(window);


window.addEventListener('DOMContentLoaded', function() {

  var model = new TodoModel();
  var form = document.querySelector('form');
  var archive = document.getElementById('archive');
  var list = document.getElementById('list');
  var todoTemplate = document.querySelector('#templates > [data-name="list"]');

  form.addEventListener('submit', function(e) {
    var textEl = e.target.querySelector('input[type="text"]');
    model.addTodo(textEl.value, false);
    textEl.value=null;
    e.preventDefault();
  });

  archive.addEventListener('click', function(e) {
    model.archiveDone();
    e.preventDefault();
  });

  model.addListener(function(model, changeType, param) {
    if ( changeType === 'removed' || changeType === 'archived') {
      redrawUI(model);
    } else if ( changeType === 'added' ) {
      drawTodo(model.todos[param], list);
    } else if ( changeType === 'stateChanged') {
      updateTodo(model.todos[param]);
    }
    updateCounters(model);
  });

  var redrawUI = function(model) {
    list.innerHTML='';
    for (var id in model.todos) {
      drawTodo(model.todos[id], list);
    }
  };
  
  var drawTodo = function(todoObj, container) {
    var el = todoTemplate.cloneNode(true);
    el.setAttribute('data-id', todoObj.id);
    container.appendChild(el);
    updateTodo(todoObj);
    var checkbox = el.querySelector('input[type="checkbox"]');
    checkbox.addEventListener('change', function(e) {
      model.setTodoState(todoObj.id, e.target.checked);
    });
  }

  var updateTodo = function(model) {
    var todoElement = list.querySelector('li[data-id="'+model.id+'"]');
    if (todoElement) {
      var checkbox = todoElement.querySelector('input[type="checkbox"]');
      var desc = todoElement.querySelector('span');
      checkbox.checked = model.isDone;
      desc.innerText = model.text;
      desc.className = "done-"+model.isDone;
    }
  }

  var updateCounters = function(model) {
    var count = 0;
    var notDone = 0;
    for (var id in model.todos) {
      count++;
      if ( ! model.todos[id].isDone ) {
        notDone ++;
      }
    }
    document.getElementById('remaining').innerText = notDone;
    document.getElementById('length').innerText = count;
  }

  updateCounters(model);

});
    

Update view

Change the AngularJS index.html or JavaScript index.html:

Angular
JavaScript
<html ng-app ng-csp>
  <head>
    <script src="angular.min.js"></script>
    <script src="controller.js"></script>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div ng-controller="TodoCtrl">
      <span>{{remaining()}} of {{todos.length}} remaining</span>
      [ <a href="" ng-click="archive()">archive</a> ]
      <ul>
        <li ng-repeat="todo in todos">
          <input type="checkbox" ng-model="todo.done">
          <span class="done-{{todo.done}}">{{todo.text}}</span>
        </li>
      </ul>
      <form ng-submit="addTodo()">
        <input type="text" ng-model="todoText" size="30"
               placeholder="add new todo here">
        <input class="btn-primary" type="submit" value="add">
      </form>
    </div>
  </body>
</html>
    
<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="todo.css">
  </head>
  <body>
    <h2>Todo</h2>
    <div>
      <span><span id="remaining"></span> of <span id="length"></span> remaining</span>
      [ <a href="" id="archive">archive</a> ]
      <ul class="unstyled" id="list">
      </ul>
      <form>
        <input type="text" size="30"
               placeholder="add new todo here">
        <input class="btn-primary" type="submit" value="add">
      </form>
    </div>

    <!-- poor man's template -->
    <div id="templates" style="display: none;">
      <li data-name="list">
        <input type="checkbox">
        <span></span>
      </li>
    </div>

    <script src="controller.js"></script>
  </body>
</html>
    

Note how the data, stored in an array inside the controller, binds to the view and is automatically updated when it is changed by the controller.

Check the results

Check the results by reloading the app: open the app, right-click and select Reload App.

Takeaways

You should also read

What's next?

In 4 - Save and Fetch Data, you will modify your Todo list app so that Todo items are saved.