blob: 00212c923ddef04c1619e8df9690a1e6c12b8d90 [file] [log] [blame]
<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&#39;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">
&lt;!doctype html&gt;
&lt;html ng-app ng-csp&gt;
&lt;head&gt;
&lt;script src=&quot;angular.min.js&quot;&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="todo.css"&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;Todo&lt;/h2&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li&gt;
&#123;&#123;todoText&#125;&#125;
&lt;/li&gt;
&lt;/ul&gt;
&lt;input type=&quot;text&quot; ng-model=&quot;todoText&quot; size="30"
placeholder=&quot;type your todo here&quot;&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
</content>
<content>
<pre data-filename="index.html">
&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;todo.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;Todo&lt;/h2&gt;
&lt;div&gt;
&lt;ul&gt;
&lt;li id=&quot;todoText&quot;&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;input type=&quot;text&quot; id=&quot;newTodo&quot; size=&quot;30&quot;
placeholder=&quot;type your todo here&quot;&gt;
&lt;/div&gt;
&lt;script src=&quot;controller.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
</content>
</tabs>
<p class="note"><b>Note:</b> The <code>ng-csp</code> directive tells Angular to run in a &quot;content security mode&quot;. You don&#39;t need this directive when using Angular v1.1.0+. We&#39;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&#39;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:&#39;learn angular&#39;, done:true},
{text:&#39;build an angular Chrome packaged app&#39;, done:false}];
$scope.addTodo = function() {
$scope.todos.push({text:$scope.todoText, done:false});
$scope.todoText = &#39;&#39;;
};
$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 &gt; [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">
&lt;html ng-app ng-csp&gt;
&lt;head&gt;
&lt;script src=&quot;angular.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;controller.js&quot;&gt;&lt;/script&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;todo.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;Todo&lt;/h2&gt;
&lt;div ng-controller=&quot;TodoCtrl&quot;&gt;
&lt;span&gt;&#123;&#123;remaining()&#125;&#125; of &#123;&#123;todos.length&#125;&#125; remaining&lt;/span&gt;
[ &lt;a href=&quot;&quot; ng-click=&quot;archive()&quot;&gt;archive&lt;/a&gt; ]
&lt;ul&gt;
&lt;li ng-repeat=&quot;todo in todos&quot;&gt;
&lt;input type=&quot;checkbox&quot; ng-model=&quot;todo.done&quot;&gt;
&lt;span class=&quot;done-&#123;&#123;todo.done&#125;&#125;&quot;&gt;&#123;&#123;todo.text&#125;&#125;&lt;/span&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;form ng-submit=&quot;addTodo()&quot;&gt;
&lt;input type=&quot;text&quot; ng-model=&quot;todoText&quot; size=&quot;30&quot;
placeholder=&quot;add new todo here&quot;&gt;
&lt;input class=&quot;btn-primary&quot; type=&quot;submit&quot; value=&quot;add&quot;&gt;
&lt;/form&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
</content>
<content>
<pre data-filename="index.html">
&lt;!doctype html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;todo.css&quot;&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h2&gt;Todo&lt;/h2&gt;
&lt;div&gt;
&lt;span&gt;&lt;span id=&quot;remaining&quot;&gt;&lt;/span&gt; of &lt;span id=&quot;length&quot;&gt;&lt;/span&gt; remaining&lt;/span&gt;
[ &lt;a href=&quot;&quot; id=&quot;archive&quot;&gt;archive&lt;/a&gt; ]
&lt;ul class=&quot;unstyled&quot; id=&quot;list&quot;&gt;
&lt;/ul&gt;
&lt;form&gt;
&lt;input type=&quot;text&quot; size=&quot;30&quot;
placeholder=&quot;add new todo here&quot;&gt;
&lt;input class=&quot;btn-primary&quot; type=&quot;submit&quot; value=&quot;add&quot;&gt;
&lt;/form&gt;
&lt;/div&gt;
&lt;!-- poor man's template --&gt;
&lt;div id=&quot;templates&quot; style=&quot;display: none;&quot;&gt;
&lt;li data-name=&quot;list&quot;&gt;
&lt;input type=&quot;checkbox&quot;&gt;
&lt;span&gt;&lt;/span&gt;
&lt;/li&gt;
&lt;/div&gt;
&lt;script src=&quot;controller.js&quot;&gt;&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</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>