| OLD | NEW |
| (Empty) |
| 1 <h1 id="lab_3_model_view_controller">Create MVC</h1> | |
| 2 | |
| 3 <p>Whenever your application grows beyond a single script with a few dozen lines
, | |
| 4 it gets harder and harder to manage without a good separation | |
| 5 of roles among app components. | |
| 6 One of the most common models for structuring a complex application, | |
| 7 no matter what language, | |
| 8 is the Model-View-Controller (MVC) and its variants, | |
| 9 like Model-View-Presentation (MVP).</p> | |
| 10 | |
| 11 <p>There are several frameworks to help apply | |
| 12 <a href="app_frameworks">MVC concepts</a> | |
| 13 to a Javascript application, and most of them, | |
| 14 as long as they are CSP compliant, can be used in a Chrome App. | |
| 15 In this lab, | |
| 16 we'll add an MVC model using both pure JavaScript and | |
| 17 the <a href="http://angularjs.org/">AngularJS</a> framework. | |
| 18 Most of the AngularJS code from this section was copied, | |
| 19 with small changes, from the AngularJS Todo tutorial.</p> | |
| 20 | |
| 21 <p class="note"><b>Note:</b> | |
| 22 Chrome Apps don't enforce any specific framework or programming style. | |
| 23 </p> | |
| 24 | |
| 25 <h2 id="simple">Create a simple view</h2> | |
| 26 | |
| 27 <h3 id="basic-mvc">Add MVC basics</h3> | |
| 28 | |
| 29 <p>If using AngularJS, download the | |
| 30 <a href="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js">An
gular script</a> | |
| 31 and save it as | |
| 32 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/angularjs/simpleview/angular.min.js">angular.min.js</a>.</p> | |
| 33 | |
| 34 <p>If using JavaScript, | |
| 35 you will need to add a very simple controller with basic MVC functionalities: | |
| 36 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/javascript/simpleview/controller.js">JavaScript controller.js</a></p> | |
| 37 | |
| 38 <h3 id="update-view">Update view</h3> | |
| 39 | |
| 40 <p>Change the <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/m
aster/lab3_mvc/angularjs/simpleview/index.html">AngularJS index.html</a> or | |
| 41 <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 sam
ple: | |
| 42 </p> | |
| 43 | |
| 44 <tabs data-group="source"> | |
| 45 | |
| 46 <header tabindex="0" data-value="angular">Angular</header> | |
| 47 <header tabindex="0" data-value="js">JavaScript</header> | |
| 48 | |
| 49 <content> | |
| 50 <pre data-filename="index.html"> | |
| 51 <!doctype html> | |
| 52 <html ng-app ng-csp> | |
| 53 <head> | |
| 54 <script src="angular.min.js"></script> | |
| 55 <link rel="stylesheet" href="todo.css"> | |
| 56 </head> | |
| 57 <body> | |
| 58 <h2>Todo</h2> | |
| 59 <div> | |
| 60 <ul> | |
| 61 <li> | |
| 62 {{todoText}} | |
| 63 </li> | |
| 64 </ul> | |
| 65 <input type="text" ng-model="todoText" size="30" | |
| 66 placeholder="type your todo here"> | |
| 67 </div> | |
| 68 </body> | |
| 69 </html> | |
| 70 </pre> | |
| 71 </content> | |
| 72 <content> | |
| 73 <pre data-filename="index.html"> | |
| 74 <!doctype html> | |
| 75 <html> | |
| 76 <head> | |
| 77 <link rel="stylesheet" href="todo.css"> | |
| 78 </head> | |
| 79 <body> | |
| 80 <h2>Todo</h2> | |
| 81 <div> | |
| 82 <ul> | |
| 83 <li id="todoText"> | |
| 84 </li> | |
| 85 </ul> | |
| 86 <input type="text" id="newTodo" size="30" | |
| 87 placeholder="type your todo here"> | |
| 88 </div> | |
| 89 <script src="controller.js"></script> | |
| 90 </body> | |
| 91 </html> | |
| 92 </pre> | |
| 93 </content> | |
| 94 </tabs> | |
| 95 | |
| 96 <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 w
hen using Angular v1.1.0+. We've included it here so that the sample works r
egardless of the Angular version in use.</p> | |
| 97 | |
| 98 <h3 id="stylesheet">Add stylesheet</h3> | |
| 99 | |
| 100 <p><a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_
mvc/angularjs/simpleview/todo.css">AngularJS todo.css</a> and | |
| 101 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/javascript/simpleview/todo.css">JavaScript todo.css</a> are the same: | |
| 102 </p> | |
| 103 | |
| 104 <pre data-filename="todo.css"> | |
| 105 body { | |
| 106 font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; | |
| 107 } | |
| 108 | |
| 109 ul { | |
| 110 list-style: none; | |
| 111 } | |
| 112 | |
| 113 button, input[type=submit] { | |
| 114 background-color: #0074CC; | |
| 115 background-image: linear-gradient(top, #08C, #05C); | |
| 116 border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); | |
| 117 text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); | |
| 118 color: white; | |
| 119 } | |
| 120 | |
| 121 .done-true { | |
| 122 text-decoration: line-through; | |
| 123 color: grey; | |
| 124 } | |
| 125 </pre> | |
| 126 | |
| 127 <h3 id="check1">Check the results</h3> | |
| 128 | |
| 129 <p> | |
| 130 Check the results by reloading the app: open the app, right-click and select Rel
oad App.</li> | |
| 131 </p> | |
| 132 | |
| 133 <h2 id="real-todo">Create real Todo list</h2> | |
| 134 | |
| 135 <p> | |
| 136 The previous sample, although interesting, is not exactly useful. | |
| 137 Let's transform it into a real Todo list, instead of a simple Todo item. | |
| 138 </p> | |
| 139 | |
| 140 <h3 id="controller">Add controller</h3> | |
| 141 | |
| 142 <p> | |
| 143 Whether using pure JavaScript or AngularJS, | |
| 144 the controller manages the Todo list: | |
| 145 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/angularjs/withcontroller/controller.js">AngularJS controller.js</a> or | |
| 146 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/javascript/withcontroller/controller.js">JavaScript controller.js</a>. | |
| 147 </p> | |
| 148 | |
| 149 <tabs data-group="source"> | |
| 150 | |
| 151 <header tabindex="0" data-value="angular">Angular</header> | |
| 152 <header tabindex="0" data-value="js">JavaScript</header> | |
| 153 | |
| 154 <content> | |
| 155 <pre data-filename="controller.js"> | |
| 156 function TodoCtrl($scope) { | |
| 157 $scope.todos = [ | |
| 158 {text:'learn angular', done:true}, | |
| 159 {text:'build an angular Chrome packaged app', done:false}]; | |
| 160 | |
| 161 $scope.addTodo = function() { | |
| 162 $scope.todos.push({text:$scope.todoText, done:false}); | |
| 163 $scope.todoText = ''; | |
| 164 }; | |
| 165 | |
| 166 $scope.remaining = function() { | |
| 167 var count = 0; | |
| 168 angular.forEach($scope.todos, function(todo) { | |
| 169 count += todo.done ? 0 : 1; | |
| 170 }); | |
| 171 return count; | |
| 172 }; | |
| 173 | |
| 174 $scope.archive = function() { | |
| 175 var oldTodos = $scope.todos; | |
| 176 $scope.todos = []; | |
| 177 angular.forEach(oldTodos, function(todo) { | |
| 178 if (!todo.done) $scope.todos.push(todo); | |
| 179 }); | |
| 180 }; | |
| 181 } | |
| 182 </pre> | |
| 183 </content> | |
| 184 <content> | |
| 185 <pre data-filename="controller.js"> | |
| 186 (function(exports) { | |
| 187 | |
| 188 var nextId = 1; | |
| 189 | |
| 190 var TodoModel = function() { | |
| 191 this.todos = {}; | |
| 192 this.listeners = []; | |
| 193 } | |
| 194 | |
| 195 TodoModel.prototype.clearTodos = function() { | |
| 196 this.todos = {}; | |
| 197 this.notifyListeners('removed'); | |
| 198 } | |
| 199 | |
| 200 TodoModel.prototype.archiveDone = function() { | |
| 201 var oldTodos = this.todos; | |
| 202 this.todos={}; | |
| 203 for (var id in oldTodos) { | |
| 204 if ( ! oldTodos[id].isDone ) { | |
| 205 this.todos[id] = oldTodos[id]; | |
| 206 } | |
| 207 } | |
| 208 this.notifyListeners('archived'); | |
| 209 } | |
| 210 | |
| 211 TodoModel.prototype.setTodoState = function(id, isDone) { | |
| 212 if ( this.todos[id].isDone != isDone ) { | |
| 213 this.todos[id].isDone = isDone; | |
| 214 this.notifyListeners('stateChanged', id); | |
| 215 } | |
| 216 } | |
| 217 | |
| 218 TodoModel.prototype.addTodo = function(text, isDone) { | |
| 219 var id = nextId++; | |
| 220 this.todos[id]={'id': id, 'text': text, 'isDone': isDone}; | |
| 221 this.notifyListeners('added', id); | |
| 222 } | |
| 223 | |
| 224 TodoModel.prototype.addListener = function(listener) { | |
| 225 this.listeners.push(listener); | |
| 226 } | |
| 227 | |
| 228 TodoModel.prototype.notifyListeners = function(change, param) { | |
| 229 var this_ = this; | |
| 230 this.listeners.forEach(function(listener) { | |
| 231 listener(this_, change, param); | |
| 232 }); | |
| 233 } | |
| 234 | |
| 235 exports.TodoModel = TodoModel; | |
| 236 | |
| 237 })(window); | |
| 238 | |
| 239 | |
| 240 window.addEventListener('DOMContentLoaded', function() { | |
| 241 | |
| 242 var model = new TodoModel(); | |
| 243 var form = document.querySelector('form'); | |
| 244 var archive = document.getElementById('archive'); | |
| 245 var list = document.getElementById('list'); | |
| 246 var todoTemplate = document.querySelector('#templates > [data-name="list"]'
); | |
| 247 | |
| 248 form.addEventListener('submit', function(e) { | |
| 249 var textEl = e.target.querySelector('input[type="text"]'); | |
| 250 model.addTodo(textEl.value, false); | |
| 251 textEl.value=null; | |
| 252 e.preventDefault(); | |
| 253 }); | |
| 254 | |
| 255 archive.addEventListener('click', function(e) { | |
| 256 model.archiveDone(); | |
| 257 e.preventDefault(); | |
| 258 }); | |
| 259 | |
| 260 model.addListener(function(model, changeType, param) { | |
| 261 if ( changeType === 'removed' || changeType === 'archived') { | |
| 262 redrawUI(model); | |
| 263 } else if ( changeType === 'added' ) { | |
| 264 drawTodo(model.todos[param], list); | |
| 265 } else if ( changeType === 'stateChanged') { | |
| 266 updateTodo(model.todos[param]); | |
| 267 } | |
| 268 updateCounters(model); | |
| 269 }); | |
| 270 | |
| 271 var redrawUI = function(model) { | |
| 272 list.innerHTML=''; | |
| 273 for (var id in model.todos) { | |
| 274 drawTodo(model.todos[id], list); | |
| 275 } | |
| 276 }; | |
| 277 | |
| 278 var drawTodo = function(todoObj, container) { | |
| 279 var el = todoTemplate.cloneNode(true); | |
| 280 el.setAttribute('data-id', todoObj.id); | |
| 281 container.appendChild(el); | |
| 282 updateTodo(todoObj); | |
| 283 var checkbox = el.querySelector('input[type="checkbox"]'); | |
| 284 checkbox.addEventListener('change', function(e) { | |
| 285 model.setTodoState(todoObj.id, e.target.checked); | |
| 286 }); | |
| 287 } | |
| 288 | |
| 289 var updateTodo = function(model) { | |
| 290 var todoElement = list.querySelector('li[data-id="'+model.id+'"]'); | |
| 291 if (todoElement) { | |
| 292 var checkbox = todoElement.querySelector('input[type="checkbox"]'); | |
| 293 var desc = todoElement.querySelector('span'); | |
| 294 checkbox.checked = model.isDone; | |
| 295 desc.innerText = model.text; | |
| 296 desc.className = "done-"+model.isDone; | |
| 297 } | |
| 298 } | |
| 299 | |
| 300 var updateCounters = function(model) { | |
| 301 var count = 0; | |
| 302 var notDone = 0; | |
| 303 for (var id in model.todos) { | |
| 304 count++; | |
| 305 if ( ! model.todos[id].isDone ) { | |
| 306 notDone ++; | |
| 307 } | |
| 308 } | |
| 309 document.getElementById('remaining').innerText = notDone; | |
| 310 document.getElementById('length').innerText = count; | |
| 311 } | |
| 312 | |
| 313 updateCounters(model); | |
| 314 | |
| 315 }); | |
| 316 </pre> | |
| 317 </content> | |
| 318 </tabs> | |
| 319 | |
| 320 <h3 id="index">Update view</h3> | |
| 321 | |
| 322 <p>Change the <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/m
aster/lab3_mvc/angularjs/withcontroller/index.html">AngularJS index.html</a> or | |
| 323 <a href="https://github.com/GoogleChrome/chrome-app-codelab/blob/master/lab3_mvc
/javascript/withcontroller/index.html">JavaScript index.html</a>: | |
| 324 </p> | |
| 325 | |
| 326 <tabs data-group="source"> | |
| 327 | |
| 328 <header tabindex="0" data-value="angular">Angular</header> | |
| 329 <header tabindex="0" data-value="js">JavaScript</header> | |
| 330 | |
| 331 <content> | |
| 332 <pre data-filename="index.html"> | |
| 333 <html ng-app ng-csp> | |
| 334 <head> | |
| 335 <script src="angular.min.js"></script> | |
| 336 <script src="controller.js"></script> | |
| 337 <link rel="stylesheet" href="todo.css"> | |
| 338 </head> | |
| 339 <body> | |
| 340 <h2>Todo</h2> | |
| 341 <div ng-controller="TodoCtrl"> | |
| 342 <span>{{remaining()}} of {{todos.lengt
h}} remaining</span> | |
| 343 [ <a href="" ng-click="archive()">archive</a&
gt; ] | |
| 344 <ul> | |
| 345 <li ng-repeat="todo in todos"> | |
| 346 <input type="checkbox" ng-model="todo.done"> | |
| 347 <span class="done-{{todo.done}}">&
#123;{todo.text}}</span> | |
| 348 </li> | |
| 349 </ul> | |
| 350 <form ng-submit="addTodo()"> | |
| 351 <input type="text" ng-model="todoText" size="
;30" | |
| 352 placeholder="add new todo here"> | |
| 353 <input class="btn-primary" type="submit" value=&q
uot;add"> | |
| 354 </form> | |
| 355 </div> | |
| 356 </body> | |
| 357 </html> | |
| 358 </pre> | |
| 359 </content> | |
| 360 <content> | |
| 361 <pre data-filename="index.html"> | |
| 362 <!doctype html> | |
| 363 <html> | |
| 364 <head> | |
| 365 <link rel="stylesheet" href="todo.css"> | |
| 366 </head> | |
| 367 <body> | |
| 368 <h2>Todo</h2> | |
| 369 <div> | |
| 370 <span><span id="remaining"></span> of <span
id="length"></span> remaining</span> | |
| 371 [ <a href="" id="archive">archive</a> ] | |
| 372 <ul class="unstyled" id="list"> | |
| 373 </ul> | |
| 374 <form> | |
| 375 <input type="text" size="30" | |
| 376 placeholder="add new todo here"> | |
| 377 <input class="btn-primary" type="submit" value=&q
uot;add"> | |
| 378 </form> | |
| 379 </div> | |
| 380 | |
| 381 <!-- poor man's template --> | |
| 382 <div id="templates" style="display: none;"> | |
| 383 <li data-name="list"> | |
| 384 <input type="checkbox"> | |
| 385 <span></span> | |
| 386 </li> | |
| 387 </div> | |
| 388 | |
| 389 <script src="controller.js"></script> | |
| 390 </body> | |
| 391 </html> | |
| 392 </pre> | |
| 393 </content> | |
| 394 </tabs> | |
| 395 | |
| 396 <p>Note how the data, stored in an array inside the controller, binds to the vie
w and is automatically updated when it is changed by the controller.</p> | |
| 397 | |
| 398 <h3 id="check2">Check the results</h3> | |
| 399 | |
| 400 <p> | |
| 401 Check the results by reloading the app: open the app, right-click and select Rel
oad App.</li> | |
| 402 </p> | |
| 403 | |
| 404 <h2 id="takeaways_">Takeaways</h2> | |
| 405 | |
| 406 <ul> | |
| 407 <li><p>Chrome Apps are | |
| 408 <a href="offline_apps">offline first</a>, | |
| 409 so the recommended way to include third-party scripts is to download | |
| 410 and package them inside your app.</p></li> | |
| 411 <li><p>You can use any framework you want, | |
| 412 as long as it complies with Content Security Policies | |
| 413 and other restrictions that Chrome Apps are enforced to follow.</p></li> | |
| 414 <li><p>MVC frameworks make your life easier. | |
| 415 Use them, specially if you want to build a non-trivial application.</p></li> | |
| 416 </ul> | |
| 417 | |
| 418 <h2 id="you_should_also_read">You should also read</h2> | |
| 419 | |
| 420 <ul> | |
| 421 <li><p><a href="angular_framework">Build Apps with AngularJS</a> tutorial</p></l
i> | |
| 422 <li><p><a href="http://angularjs.org/">AngularJS Todo</a> tutorial</p></li> | |
| 423 </ul> | |
| 424 | |
| 425 <h2 id="what_39_s_next_">What's next?</h2> | |
| 426 | |
| 427 <p>In <a href="app_codelab5_data">4 - Save and Fetch Data</a>, | |
| 428 you will modify your Todo list app so that Todo items are saved.</p> | |
| OLD | NEW |