| <h1 id="lab_3_model_view_controller">Create MVC</h1> |
| |
| <p>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).</p> |
| |
| <p>There are several frameworks to help apply |
| <a href="app_frameworks">MVC concepts</a> |
| 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 <a href="http://angularjs.org/">AngularJS</a> framework. |
| Most of the AngularJS code from this section was copied, |
| with small changes, from the AngularJS Todo tutorial.</p> |
| |
| <p class="note"><b>Note:</b> |
| Chrome Apps don't enforce any specific framework or programming style. |
| </p> |
| |
| <h2 id="simple">Create a simple view</h2> |
| |
| <h3 id="basic-mvc">Add MVC basics</h3> |
| |
| <p>If using AngularJS, download the |
| <a href="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">Angular script</a> |
| and save it as |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/angularjs/simpleview/angular.min.js">angular.min.js</a>.</p> |
| |
| <p>If using JavaScript, |
| you will need to add a very simple controller with basic MVC functionalities: |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/javascript/simpleview/controller.js">JavaScript controller.js</a></p> |
| |
| <h3 id="update-view">Update view</h3> |
| |
| <p>Change the <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/angularjs/simpleview/index.html">AngularJS index.html</a> or |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/javascript/simpleview/index.html">JavaScript index.html</a> to use a simple sample: |
| </p> |
| |
| <tabs data-group="source"> |
| |
| <header tabindex="0" data-value="angular">Angular</header> |
| <header tabindex="0" data-value="js">JavaScript</header> |
| |
| <content> |
| <pre data-filename="index.html"> |
| <!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> |
| </pre> |
| </content> |
| <content> |
| <pre data-filename="index.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> |
| </pre> |
| </content> |
| </tabs> |
| |
| <p class="note"><b>Note:</b> The <code>ng-csp</code> 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.</p> |
| |
| <h3 id="stylesheet">Add stylesheet</h3> |
| |
| <p><a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/angularjs/simpleview/todo.css">AngularJS todo.css</a> and |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/javascript/simpleview/todo.css">JavaScript todo.css</a> are the same: |
| </p> |
| |
| <pre data-filename="todo.css"> |
| 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; |
| } |
| </pre> |
| |
| <h3 id="check1">Check the results</h3> |
| |
| <p> |
| Check the results by reloading the app: open the app, right-click and select Reload App.</li> |
| </p> |
| |
| <h2 id="real-todo">Create real Todo list</h2> |
| |
| <p> |
| The previous sample, although interesting, is not exactly useful. |
| Let's transform it into a real Todo list, instead of a simple Todo item. |
| </p> |
| |
| <h3 id="controller">Add controller</h3> |
| |
| <p> |
| Whether using pure JavaScript or AngularJS, |
| the controller manages the Todo list: |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/angularjs/withcontroller/controller.js">AngularJS controller.js</a> or |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/javascript/withcontroller/controller.js">JavaScript controller.js</a>. |
| </p> |
| |
| <tabs data-group="source"> |
| |
| <header tabindex="0" data-value="angular">Angular</header> |
| <header tabindex="0" data-value="js">JavaScript</header> |
| |
| <content> |
| <pre data-filename="controller.js"> |
| 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); |
| }); |
| }; |
| } |
| </pre> |
| </content> |
| <content> |
| <pre data-filename="controller.js"> |
| (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); |
| |
| }); |
| </pre> |
| </content> |
| </tabs> |
| |
| <h3 id="index">Update view</h3> |
| |
| <p>Change the <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/angularjs/withcontroller/index.html">AngularJS index.html</a> or |
| <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc/javascript/withcontroller/index.html">JavaScript index.html</a>: |
| </p> |
| |
| <tabs data-group="source"> |
| |
| <header tabindex="0" data-value="angular">Angular</header> |
| <header tabindex="0" data-value="js">JavaScript</header> |
| |
| <content> |
| <pre data-filename="index.html"> |
| <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> |
| </pre> |
| </content> |
| <content> |
| <pre data-filename="index.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> |
| </pre> |
| </content> |
| </tabs> |
| |
| <p>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.</p> |
| |
| <h3 id="check2">Check the results</h3> |
| |
| <p> |
| Check the results by reloading the app: open the app, right-click and select Reload App.</li> |
| </p> |
| |
| <h2 id="takeaways_">Takeaways</h2> |
| |
| <ul> |
| <li><p>Chrome Apps are |
| <a href="offline_apps">offline first</a>, |
| so the recommended way to include third-party scripts is to download |
| and package them inside your app.</p></li> |
| <li><p>You can use any framework you want, |
| as long as it complies with Content Security Policies |
| and other restrictions that Chrome Apps are enforced to follow.</p></li> |
| <li><p>MVC frameworks make your life easier. |
| Use them, specially if you want to build a non-trivial application.</p></li> |
| </ul> |
| |
| <h2 id="you_should_also_read">You should also read</h2> |
| |
| <ul> |
| <li><p><a href="angular_framework">Build Apps with AngularJS</a> tutorial</p></li> |
| <li><p><a href="http://angularjs.org/">AngularJS Todo</a> tutorial</p></li> |
| </ul> |
| |
| <h2 id="what_39_s_next_">What's next?</h2> |
| |
| <p>In <a href="app_codelab5_data">4 - Save and Fetch Data</a>, |
| you will modify your Todo list app so that Todo items are saved.</p> |