Commit 531d16dd authored by Chris Price's avatar Chris Price Committed by Sindre Sorhus

Closes #172: Closure app update. Fixes #155

parent 265fa96d
# TodoMVC Closure Architecture Example
## Introduction
An example making use of the [Closure toolkit](https://developers.google.com/closure/). Note this project breaks with the convention of the others and uses spaces in place of tabs withing JavaScript files. This is to comply with the Google style guidelines which the Closure Linter enforces (see Linting below).
## Running
A third party build tool called [Plovr](http://plovr.com/) is used to make running and compiling the code easier. To serve the code for development purposes (the example should run in compiled mode without using Plovr), first download the latest stable version from the [Plovr Google Code project](http://code.google.com/p/plovr/downloads/list) (at the time of writing plovr-4b3caf2b7d84.jar). Copy the file into the build folder, rename it plovr.jar and run the following command from this folder -
`java -jar build/plovr.jar serve plovr.json`
You'll also need to change the HTML file so that it references the served files instead of the compiled version (**make sure you comment out the compiled version otherwise it will not work**), to do this remove the compiled script reference and add the following -
`<script type="text/javascript" src="http://localhost:9810/compile?id=todomvc&mode=RAW"></script>`
This will serve up the javascript files in RAW mode which is ideal for rapid development and debugging. To run the compiler, and therefore all the associated type checks etc., change RAW for ADVANCED -
`<script type="text/javascript" src="http://localhost:9810/compile?id=todomvc&mode=ADVANCED"></script>`
## Linting
Whilst Plovr features many of the tools from the Closure toolkit, one very useful one that's missing is the linter. The linter checks for common mistakes in your code, e.g. unused dependencies, whitespace errors. One restriction with the linter is that it will only permit code that adheres to the [Google JavaScript style guide](http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml). In this case that means that we break with the project conventions and use space indentation instead of tabs.
The linter must be installed before use, the installation package is included in the build folder and the instructions are available on the [linter homepage](https://developers.google.com/closure/utilities/). Once installed run the following to check for errors -
`find . -name *.js | xargs gjslint`
(or whatever floats your OSs boat)
## Compiling
To compile the code from the command line run Plovr like so -
`java -jar build/plovr.jar build plovr.json > js/compiled.js`
This will overwrite the js/compiled.js file with the new version, be sure to change the script tag reference in the HTML page.
## Credits
Template by [Sindre Sorhus](http://github.com/sindresorhus)
Created by [Chris Price](http://www.scottlogic.co.uk/blog/chris/)
Part of [TodoMVC](http://todomvc.com)
## License
Public Domain
\ No newline at end of file
plovr-4b3caf2b7d84
\ No newline at end of file
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Closure • TodoMVC</title>
<link href="css/todos.css" rel="stylesheet">
<link rel="stylesheet" href="../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<div id="todoapp">
<div class="title">
<h1>Todos</h1>
</div>
<div class="content">
<div id="create-todo">
<input id="new-todo" placeholder="What needs to be done?" type="text">
</div>
<div id="todos">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
</ul>
</div>
<div id="todo-stats">
</div>
</div>
</div>
<ul id="instructions">
<li>Click to edit a todo</li>
</section>
<footer id="footer">
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
<a href="#/active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
</li>
</ul>
<div id="credits">
Created by <a href="http://www.scottlogic.co.uk/blog/chris/">Chris Price</a>
</div>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://github.com/sindresorhus">Sindre Sorhus</a></p>
<p>Created by <a href="http://www.scottlogic.co.uk/blog/chris/">Chris Price</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="../../assets/base.js"></script>
<!-- The compiled version (to update run java -jar build/plovr.jar build plovr.json > web/compiled.js) -->
<!-- The compiled version (to update run java -jar build/plovr.jar build plovr.json > js/compiled.js) -->
<script type="text/javascript" src="js/compiled.js"></script>
<!-- The RAW development version (to serve the files run java -jar build/plovr.jar serve plovr.json) -->
<!-- <script type="text/javascript" src="http://localhost:9810/compile?id=todomvc&mode=RAW"></script> -->
......
goog.provide('todomvc');
goog.require('goog.History');
goog.require('goog.array');
goog.require('goog.dom.query');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.storage.Storage');
goog.require('goog.storage.mechanism.mechanismfactory');
goog.require('goog.string');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('todomvc.model.ToDoItem');
goog.require('todomvc.model.ToDoItemStore');
goog.require('todomvc.view');
goog.require('todomvc.view.ClearCompletedControlRenderer');
goog.require('todomvc.view.ItemCountControlRenderer');
goog.require('todomvc.view.ToDoItemControl');
goog.require('todomvc.view.ToDoListContainer');
/**
* @fileoverview The controller/business logic for the application.
*
* This file creates the interface and marshals changes from the interface
* to the model and back.
*/
/**
* @type {todomvc.model.ToDoItemStore}
*/
var itemStore = new todomvc.model.ToDoItemStore();
itemStore.addEventListener(todomvc.model.ToDoItemStore.ChangeEventType,
redraw);
/**
* @type {todomvc.view.ToDoListContainer}
*/
var container = new todomvc.view.ToDoListContainer();
container.decorate(document.getElementById('todo-list'));
/**
* @type {Element}
*/
var main = document.getElementById('main');
/**
* @type {Element}
*/
var footer = document.getElementById('footer');
/**
* @type {goog.ui.Control}
*/
var itemCountControl = new goog.ui.Control(null,
todomvc.view.ItemCountControlRenderer.getInstance());
itemCountControl.render(footer);
/**
* @type {goog.ui.Control}
*/
var clearCompletedControl = new goog.ui.Control(null,
todomvc.view.ClearCompletedControlRenderer.getInstance());
clearCompletedControl.render(footer);
goog.events.listen(clearCompletedControl,
goog.ui.Component.EventType.ACTION, function(e) {
// go backwards to avoid collection modification problems
goog.array.forEachRight(itemStore.getAll(), function(model) {
if (model.isDone()) {
itemStore.remove(model);
}
});
});
/**
* @type {Element}
*/
var toggleAll = document.getElementById('toggle-all');
goog.events.listen(toggleAll, goog.events.EventType.CLICK, function(e) {
/**
* @type {boolean}
*/
var state = toggleAll.checked;
goog.array.forEach(itemStore.getAll(), function(model) {
/**
* @type {!todomvc.model.ToDoItem}
*/
var updatedModel = new todomvc.model.ToDoItem(
model.getNote(), state, model.getId());
itemStore.addOrUpdate(updatedModel);
});
});
/**
* Enum for the three possible route values
* @enum {!string}
*/
todomvc.Route = {
ALL: '/',
ACTIVE: '/active',
COMPLETED: '/completed'
};
/**
* @type {!todomvc.Route}
*/
var currentRoute = todomvc.Route.ALL;
/**
* @type {!goog.History}
*/
var history = new goog.History();
goog.events.listen(history, goog.history.EventType.NAVIGATE,
function(e) {
// constrain the route to be one of the enum values
switch (e.token) {
case todomvc.Route.ALL:
case todomvc.Route.ACTIVE:
case todomvc.Route.COMPLETED:
if (e.token !== currentRoute) {
currentRoute = e.token;
redraw();
}
break;
default:
history.replaceToken(todomvc.Route.ALL);
break;
}
});
function redraw() {
container.removeChildren(true);
/**
* @type {Array.<todomvc.model.ToDoItem>}
*/
var items = itemStore.getAll();
goog.array.forEach(items, function(item) {
// filter based on current route
if ((currentRoute === todomvc.Route.ACTIVE && item.isDone()) ||
(currentRoute === todomvc.Route.COMPLETED && !item.isDone())) {
return;
}
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = new todomvc.view.ToDoItemControl();
control.setContent(item.getNote());
control.setChecked(item.isDone());
control.setModel(item);
container.addChild(control, true);
});
var doneCount = /** @type {number} */
(goog.array.reduce(items, function(count, model) {
return model.isDone() ? count + 1 : count;
}, 0));
var remainingCount = items.length - (doneCount);
toggleAll.checked = remainingCount === 0;
itemCountControl.setContent(remainingCount.toString());
clearCompletedControl.setContent(doneCount.toString());
clearCompletedControl.setVisible(doneCount > 0);
goog.style.showElement(main, items.length > 0);
goog.style.showElement(footer, items.length > 0);
/**
* @type {Array.<Element>}
*/
var routeLinks = /** @type {Array.<Element>} */
(goog.dom.query('#filters a'));
goog.array.forEach(routeLinks, function(link, i) {
if ((currentRoute === todomvc.Route.ALL && i === 0) ||
(currentRoute === todomvc.Route.ACTIVE && i === 1) ||
(currentRoute === todomvc.Route.COMPLETED && i === 2)) {
link.className = 'selected';
} else {
link.className = '';
}
});
}
goog.events.listen(container,
todomvc.view.ToDoItemControl.EventType.EDIT, function(e) {
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = e.target;
/**
* @type {todomvc.model.ToDoItem}
*/
var originalModel = /**@type {todomvc.model.ToDoItem} */
(control.getModel());
/**
* @type {!todomvc.model.ToDoItem}
*/
var updatedModel = new todomvc.model.ToDoItem(
(/**@type {!string} */ control.getContent()),
(/**@type {!boolean} */ control.isChecked()),
originalModel.getId());
itemStore.addOrUpdate(updatedModel);
});
goog.events.listen(container,
todomvc.view.ToDoItemControl.EventType.DESTROY, function(e) {
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = e.target;
/**
* @type {todomvc.model.ToDoItem}
*/
var model = (/**@type {todomvc.model.ToDoItem} */ control.getModel());
if (model !== null) {
itemStore.remove(model);
}
});
/**
* @type {Element}
*/
var newToDo = document.getElementById('new-todo');
goog.events.listen(newToDo, goog.events.EventType.KEYUP, function(e) {
if (e.keyCode !== goog.events.KeyCodes.ENTER) {
return;
}
// get the text
var value = goog.string.trim(newToDo.value);
if (value === '') {
return;
}
// clear the input box
newToDo.value = '';
// create the item
itemStore.addOrUpdate(new todomvc.model.ToDoItem(value));
});
itemStore.load();
history.setEnabled(true);
goog.require('goog.array');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('todomvc.model.ToDoItem');
goog.require('todomvc.view');
goog.require('todomvc.view.ClearCompletedControlRenderer');
goog.require('todomvc.view.ItemCountControlRenderer');
goog.require('todomvc.view.ToDoItemControl');
goog.require('todomvc.view.ToDoListContainer');
/**
* @fileoverview The controller/business logic for the application.
*
* This file creates the interface and marshalls changes from the interface to the model and back.
*/
/**
* @type {Array.<todomvc.model.ToDoItem>}
*/
var items = [];
/**
* @type {Element}
*/
var todoStats = document.getElementById('todo-stats');
/**
* @type {goog.ui.Control}
*/
var itemCountControl = new goog.ui.Control(null, todomvc.view.ItemCountControlRenderer.getInstance());
itemCountControl.render(todoStats);
/**
* @type {goog.ui.Control}
*/
var clearCompletedControl = new goog.ui.Control(null, todomvc.view.ClearCompletedControlRenderer.getInstance());
clearCompletedControl.render(todoStats);
goog.events.listen(clearCompletedControl, goog.ui.Component.EventType.ACTION, function(e) {
// go backwards to avoid collection modification problems
goog.array.forEachRight(items, function(model) {
if (model.isDone()) {
goog.array.remove(items, model);
// do optimised model view sync
container.forEachChild(function(control) {
if (control.getModel() === model) {
container.removeChild(control, true);
}
});
}
});
updateStats();
});
function updateStats() {
var doneCount = goog.array.reduce(items, function(count, model) {
return model.isDone() ? count + 1 : count;
}, 0);
var remainingCount = items.length - (/**@type {number}*/ doneCount);
itemCountControl.setContent((/**@type {string}*/ remainingCount));
itemCountControl.setVisible(remainingCount > 0);
clearCompletedControl.setContent((/**@type {string}*/ doneCount));
clearCompletedControl.setVisible((/**@type {number}*/ doneCount) > 0);
}
updateStats();
/**
* @type {todomvc.view.ToDoListContainer}
*/
var container = new todomvc.view.ToDoListContainer();
container.decorate(document.getElementById('todo-list'));
goog.events.listen(container, todomvc.view.ToDoItemControl.EventType.EDIT, function(e) {
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = e.target;
/**
* @type {todomvc.model.ToDoItem}
*/
var model = (/**@type {todomvc.model.ToDoItem} */ control.getModel());
// do optimised model view sync
model.setNote((/**@type {!string} */ control.getContent()));
model.setDone((/**@type {!boolean} */ control.isChecked()));
updateStats();
});
goog.events.listen(container, todomvc.view.ToDoItemControl.EventType.DESTROY, function(e) {
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = e.target;
/**
* @type {todomvc.model.ToDoItem}
*/
var model = (/**@type {todomvc.model.ToDoItem} */ control.getModel());
// do optimised model view sync
goog.array.remove(items, model);
container.removeChild(control, true);
updateStats();
});
/**
* @type {Element}
*/
var newToDo = document.getElementById('new-todo');
goog.events.listen(newToDo, goog.events.EventType.KEYUP, function(e) {
if (e.keyCode === goog.events.KeyCodes.ENTER) {
/**
* @type {todomvc.model.ToDoItem}
*/
var model = new todomvc.model.ToDoItem(newToDo.value);
/**
* @type {todomvc.view.ToDoItemControl}
*/
var control = new todomvc.view.ToDoItemControl();
// do optimised model view sync
items.push(model);
control.setContent(model.getNote());
control.setChecked(model.isDone());
control.setModel(model);
container.addChild(control, true);
// clear the input box
newToDo.value = '';
updateStats();
}
});
\ No newline at end of file
......@@ -3,11 +3,12 @@ goog.provide('todomvc.model.ToDoItem');
/**
* The model object representing a todo item.
*
* @param {!string} note the text associated with this item
* @param {!boolean=} opt_done is this item complete? defaults to false
* @param {!string} note the text associated with this item.
* @param {!boolean=} opt_done is this item complete? defaults to false.
* @param {!number=} opt_id the id for the item defaults to 0 meaning undefined.
* @constructor
*/
todomvc.model.ToDoItem = function(note, opt_done) {
todomvc.model.ToDoItem = function(note, opt_done, opt_id) {
/**
* note the text associated with this item
* @private
......@@ -21,10 +22,17 @@ todomvc.model.ToDoItem = function(note, opt_done) {
* @type {!boolean}
*/
this.done_ = opt_done || false;
/**
* the id for the item, or 0 if it is not yet defined
* @private
* @type {!number}
*/
this.id_ = opt_id || 0;
};
/**
* @return {!string} the text associated with this item
* @return {!string} the text associated with this item.
*/
todomvc.model.ToDoItem.prototype.getNote = function() {
return this.note_;
......@@ -38,7 +46,14 @@ todomvc.model.ToDoItem.prototype.isDone = function() {
};
/**
* @param {!string} note the text associated with this item
* @return {!number} the id for the item, or 0 if it is not yet defined.
*/
todomvc.model.ToDoItem.prototype.getId = function() {
return this.id_;
};
/**
* @param {!string} note the text associated with this item.
*/
todomvc.model.ToDoItem.prototype.setNote = function(note) {
this.note_ = note;
......@@ -50,3 +65,10 @@ todomvc.model.ToDoItem.prototype.setNote = function(note) {
todomvc.model.ToDoItem.prototype.setDone = function(done) {
this.done_ = done;
};
/**
* @param {!number} id the id for the item, or 0 if it is not yet defined.
*/
todomvc.model.ToDoItem.prototype.setId = function(id) {
this.id_ = id;
};
goog.provide('todomvc.model.ToDoItemStore');
goog.require('goog.array');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.storage.Storage');
goog.require('goog.storage.mechanism.mechanismfactory');
goog.require('goog.string');
goog.require('goog.ui.Component');
goog.require('goog.ui.Control');
goog.require('todomvc.model.ToDoItem');
/**
* @constructor
* @extends {goog.events.EventTarget}
*/
todomvc.model.ToDoItemStore = function() {
var mechanism = goog.storage.mechanism.mechanismfactory
.createHTML5LocalStorage();
/**
* @type {goog.storage.Storage}
* @private
*/
this.storage_ = mechanism ? new goog.storage.Storage(mechanism) : null;
/**
* @type {!Array.<todomvc.model.ToDoItem>}
* @private
*/
this.items_ = [];
/**
* Fundamentally flawed approach to ID-ing but fine for demo
* @type {!number}
* @private
*/
this.maxId_ = 0;
};
goog.inherits(todomvc.model.ToDoItemStore, goog.events.EventTarget);
/**
* Load item list from storage
*/
todomvc.model.ToDoItemStore.prototype.load = function() {
if (!this.storage_) {
this.notify_(false);
return; // no storage = no loading!
}
goog.array.clear(this.items_);
/**
* @type {Array.<*>}
*/
var serializedItems = /** @type {Array.<*>} */
(this.storage_.get('todos-closure'));
if (!serializedItems) {
this.notify_(false);
return; // nothing in storage
}
goog.array.forEach(serializedItems, function(serializedItem) {
var item = new todomvc.model.ToDoItem(serializedItem['title'],
serializedItem['completed'], serializedItem['id']);
if (item.getId() > this.maxId_) {
this.maxId_ = item.getId();
}
this.items_.push(item);
}, this);
this.notify_(false);
};
/**
* @param {!todomvc.model.ToDoItem} updatedItem A prototype model to update.
*/
todomvc.model.ToDoItemStore.prototype.addOrUpdate = function(updatedItem) {
var idx = goog.array.findIndex(this.items_, function(item) {
return updatedItem.getId() === item.getId();
});
if (idx === -1) {
if (updatedItem.getId() === 0) {
updatedItem.setId(++this.maxId_);
}
this.items_.push(updatedItem);
} else {
this.items_[idx] = updatedItem;
}
this.notify_();
};
/**
* @param {!todomvc.model.ToDoItem} itemToRemove A prototype model to remove.
*/
todomvc.model.ToDoItemStore.prototype.remove = function(itemToRemove) {
goog.array.removeIf(this.items_, function(item) {
return itemToRemove.getId() === item.getId();
});
this.notify_();
};
/**
* @param {boolean=} opt_save whether to save to storage, defaults to true.
* @private
*/
todomvc.model.ToDoItemStore.prototype.notify_ = function(opt_save) {
// TODO delay until all changes have been made
if (!goog.isDef(opt_save) || opt_save) {
this.save_();
}
this.dispatchEvent(new todomvc.model.ToDoItemStore.ChangeEvent(this));
};
/**
* @return {Array.<todomvc.model.ToDoItem>} All of the stored items.
*/
todomvc.model.ToDoItemStore.prototype.getAll = function() {
return this.items_;
};
/**
* @private
*/
todomvc.model.ToDoItemStore.prototype.save_ = function() {
if (!this.storage_) {
return; // no storage = no saving!
}
/**
* @type {Array.<*>}
*/
var serializedItems = [];
goog.array.forEach(this.items_, function(item) {
serializedItems.push({
'completed' : item.isDone(),
'title': item.getNote(),
'id' : item.getId()
});
});
this.storage_.set('todos-closure', serializedItems);
};
/**
* @const
*/
todomvc.model.ToDoItemStore.ChangeEventType = 'change';
/**
* @constructor
* @extends {goog.events.Event}
* @param {todomvc.model.ToDoItemStore} target The item store.
*/
todomvc.model.ToDoItemStore.ChangeEvent = function(target) {
goog.events.Event.call(this,
todomvc.model.ToDoItemStore.ChangeEventType, target);
};
goog.inherits(todomvc.model.ToDoItemStore.ChangeEvent, goog.events.Event);
......@@ -13,7 +13,8 @@ goog.require('goog.ui.ControlRenderer');
todomvc.view.ClearCompletedControlRenderer = function() {
goog.ui.ControlRenderer.call(this);
};
goog.inherits(todomvc.view.ClearCompletedControlRenderer, goog.ui.ControlRenderer);
goog.inherits(todomvc.view.ClearCompletedControlRenderer,
goog.ui.ControlRenderer);
// add getInstance method to todomvc.view.ClearCompletedControlRenderer
goog.addSingletonGetter(todomvc.view.ClearCompletedControlRenderer);
......@@ -22,9 +23,10 @@ goog.addSingletonGetter(todomvc.view.ClearCompletedControlRenderer);
* @param {goog.ui.Control} control Control to render.
* @return {Element} Root element for the control.
*/
todomvc.view.ClearCompletedControlRenderer.prototype.createDom = function(control) {
todomvc.view.ClearCompletedControlRenderer.prototype.createDom =
function(control) {
var html = todomvc.view.clearCompleted({
number : control.getContent()
number: control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
......@@ -35,17 +37,19 @@ todomvc.view.ClearCompletedControlRenderer.prototype.createDom = function(contro
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
todomvc.view.ClearCompletedControlRenderer.prototype.canDecorate = function(element) {
todomvc.view.ClearCompletedControlRenderer.prototype.canDecorate =
function(element) {
return false;
};
/**
* @param {Element} element Element to populate.
* @param {goog.ui.ControlContent} content Text caption or DOM
* @param {goog.ui.ControlContent} content Text caption or DOM.
*/
todomvc.view.ClearCompletedControlRenderer.prototype.setContent = function(element, content) {
todomvc.view.ClearCompletedControlRenderer.prototype.setContent =
function(element, content) {
element.innerHTML = todomvc.view.clearCompletedInner({
number : content
number: content
});
};
......@@ -56,7 +60,8 @@ todomvc.view.ClearCompletedControlRenderer.prototype.setContent = function(eleme
* @param {goog.ui.Component.State} state State to enable or disable.
* @param {boolean} enable Whether the control is entering or exiting the state.
*/
todomvc.view.ClearCompletedControlRenderer.prototype.setState = function(control, state, enable) {
todomvc.view.ClearCompletedControlRenderer.prototype.setState =
function(control, state, enable) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
......
......@@ -24,7 +24,7 @@ goog.addSingletonGetter(todomvc.view.ItemCountControlRenderer);
*/
todomvc.view.ItemCountControlRenderer.prototype.createDom = function(control) {
var html = todomvc.view.itemCount({
number : control.getContent()
number: control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
......@@ -35,17 +35,19 @@ todomvc.view.ItemCountControlRenderer.prototype.createDom = function(control) {
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
todomvc.view.ItemCountControlRenderer.prototype.canDecorate = function(element) {
todomvc.view.ItemCountControlRenderer.prototype.canDecorate =
function(element) {
return false;
};
/**
* @param {Element} element Element to populate.
* @param {goog.ui.ControlContent} content Text caption or DOM
* @param {goog.ui.ControlContent} content Text caption or DOM.
*/
todomvc.view.ItemCountControlRenderer.prototype.setContent = function(element, content) {
todomvc.view.ItemCountControlRenderer.prototype.setContent =
function(element, content) {
element.innerHTML = todomvc.view.itemCountInner({
number : content
number: content
});
};
......@@ -56,7 +58,8 @@ todomvc.view.ItemCountControlRenderer.prototype.setContent = function(element, c
* @param {goog.ui.Component.State} state State to enable or disable.
* @param {boolean} enable Whether the control is entering or exiting the state.
*/
todomvc.view.ItemCountControlRenderer.prototype.setState = function(control, state, enable) {
todomvc.view.ItemCountControlRenderer.prototype.setState =
function(control, state, enable) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
......
......@@ -2,21 +2,24 @@ goog.provide('todomvc.view.ToDoItemControl');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.KeyCodes');
goog.require('goog.string');
goog.require('goog.ui.Component.State');
goog.require('goog.ui.Control');
goog.require('todomvc.view.ToDoItemControlRenderer');
/**
* A control representing each item in the todo list. It makes use of the CHECKED and SELECTED states to represent being
* done and being in edit mode.
* A control representing each item in the todo list. It makes use of the
* CHECKED and SELECTED states to represent being done and being in edit mode.
*
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for document interaction.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper,
* used for document interaction.
* @constructor
* @extends {goog.ui.Control}
*/
todomvc.view.ToDoItemControl = function(opt_domHelper) {
goog.ui.Control.call(this, "", todomvc.view.ToDoItemControlRenderer
goog.ui.Control.call(this, '', todomvc.view.ToDoItemControlRenderer
.getInstance(), opt_domHelper);
// enable CHECKED and SELECTED states
......@@ -27,15 +30,17 @@ todomvc.view.ToDoItemControl = function(opt_domHelper) {
this.setAutoStates(goog.ui.Component.State.CHECKED, false);
this.setAutoStates(goog.ui.Component.State.SELECTED, false);
// allow text selection within this control
// allow text selection
this.setAllowTextSelection(true);
};
goog.inherits(todomvc.view.ToDoItemControl, goog.ui.Control);
/**
* The event types this control dispatches.
*/
todomvc.view.ToDoItemControl.EventType = {
EDIT: "edit",
DESTROY: "destroy"
EDIT: 'edit',
DESTROY: 'destroy'
};
......@@ -54,13 +59,30 @@ todomvc.view.ToDoItemControl.prototype.enterDocument = function() {
function(e) {
e.preventDefault();
});
this.getHandler().listen(this.getElement(), goog.events.EventType.DBLCLICK,
function(e) {
this.setSelected(true);
});
/**
* @type {Element}
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
this.getHandler().listen(inputElement, goog.events.EventType.KEYUP,
function(e) {
var be = e.getBrowserEvent();
if (be.keyCode === goog.events.KeyCodes.ENTER) {
this.setFocused(false);
}
});
};
/**
* Returns the renderer used by this component to render itself or to decorate
* an existing element.
*
* @return {todomvc.view.ToDoItemControlRenderer} Renderer used by the component
* @return {todomvc.view.ToDoItemControlRenderer} Renderer used by the
* component.
*/
todomvc.view.ToDoItemControl.prototype.getRenderer = function() {
return (/**@type {todomvc.view.ToDoItemControlRenderer}*/ this.renderer_);
......@@ -82,15 +104,13 @@ todomvc.view.ToDoItemControl.prototype.handleMouseUp = function(e) {
} else if (e.target === this.getRenderer().getDestroyElement(
this.getElement())) {
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.DESTROY);
} else if (!this.isSelected()) {
this.setSelected(true);
}
}
};
/**
* Override the behaviour when the control is unfocused.
* @param {boolean} focused
* @param {boolean} focused is focused?
*/
todomvc.view.ToDoItemControl.prototype.setFocused = function(focused) {
todomvc.view.ToDoItemControl.superClass_.setFocused.call(this, focused);
......@@ -100,18 +120,24 @@ todomvc.view.ToDoItemControl.prototype.setFocused = function(focused) {
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
this.setContent(inputElement.value);
var value = goog.string.trim(inputElement.value);
if (value === '') {
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.DESTROY);
} else {
this.setContent(value);
this.setSelected(false);
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.EDIT);
}
}
};
/**
* Override the behaviour to switch to editing mode when the control is selected
* @param {boolean} selected
* @param {boolean} selected is selected?
*/
todomvc.view.ToDoItemControl.prototype.setSelected = function(selected) {
todomvc.view.ToDoItemControl.superClass_.setSelected.call(this, selected);
// populate the input box when selected
if (selected) {
/**
* @type {Element}
......@@ -119,6 +145,6 @@ todomvc.view.ToDoItemControl.prototype.setSelected = function(selected) {
var inputElement = this.getRenderer().getInputElement(
this.getElement());
inputElement.value = this.getContent();
inputElement.focus();
inputElement.select();
}
};
......@@ -4,8 +4,8 @@ goog.require('goog.ui.Component.State');
goog.require('goog.ui.ControlRenderer');
/**
* The renderer for the ToDoItemControl which has knowledge of the DOM structure of the Control and the applicable CSS
* classes.
* The renderer for the ToDoItemControl which has knowledge of the DOM
* structure of the Control and the applicable CSS classes.
*
* @constructor
* @extends {goog.ui.ControlRenderer}
......@@ -24,10 +24,13 @@ goog.addSingletonGetter(todomvc.view.ToDoItemControlRenderer);
*/
todomvc.view.ToDoItemControlRenderer.prototype.createDom = function(control) {
var html = todomvc.view.toDoItem({
content : control.getContent()
content: control.getContent(),
checked: control.isChecked()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
this.setState(control, /** @type {goog.ui.Component.State} */
(control.getState()), true);
return element;
};
......@@ -38,16 +41,17 @@ todomvc.view.ToDoItemControlRenderer.prototype.createDom = function(control) {
* @param {goog.ui.Component.State} state State to enable or disable.
* @param {boolean} enable Whether the control is entering or exiting the state.
*/
todomvc.view.ToDoItemControlRenderer.prototype.setState = function(control, state, enable) {
todomvc.view.ToDoItemControlRenderer.prototype.setState =
function(control, state, enable) {
var element = control.getElement();
if (element) {
switch (state) {
case goog.ui.Component.State.CHECKED:
this.enableClassName(control, "done", enable);
this.enableClassName(control, 'done', enable);
this.getCheckboxElement(element).checked = enable;
break;
case goog.ui.Component.State.SELECTED:
this.enableClassName(control, "editing", enable);
this.enableClassName(control, 'editing', enable);
break;
}
......@@ -63,7 +67,8 @@ todomvc.view.ToDoItemControlRenderer.prototype.setState = function(control, stat
* returned.
* @return {Element} The key event target.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getKeyEventTarget = function(control) {
todomvc.view.ToDoItemControlRenderer.prototype.getKeyEventTarget =
function(control) {
return this.getInputElement(control.getElement());
};
......@@ -76,7 +81,7 @@ todomvc.view.ToDoItemControlRenderer.prototype.getKeyEventTarget = function(cont
*/
todomvc.view.ToDoItemControlRenderer.prototype.getDisplayElement = function(
element) {
return element ? element.childNodes[0].childNodes[0] : null;
return element ? element.childNodes[0] : null;
};
/**
......@@ -125,5 +130,5 @@ todomvc.view.ToDoItemControlRenderer.prototype.getDestroyElement = function(
*/
todomvc.view.ToDoItemControlRenderer.prototype.getInputElement = function(
element) {
return element ? element.childNodes[0].childNodes[1].childNodes[0] : null;
return element ? element.childNodes[1] : null;
};
......@@ -5,9 +5,11 @@ goog.require('goog.ui.Container');
goog.require('todomvc.view.ToDoListContainerRenderer');
/**
* A container for the ToDoItemControls, overridden to support keyboard focus on child controls.
* A container for the ToDoItemControls, overridden to support keyboard focus
* on child controls.
*
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for document interaction.
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for
* document interaction.
* @constructor
* @extends {goog.ui.Container}
*/
......
......@@ -5,7 +5,8 @@ goog.require('goog.ui.Container');
goog.require('goog.ui.ContainerRenderer');
/**
* A renderer for the container, overridden to support keyboard focus on child controls.
* A renderer for the container, overridden to support keyboard focus
* on child controls.
* @constructor
* @extends {goog.ui.ContainerRenderer}
*/
......@@ -20,7 +21,8 @@ goog.addSingletonGetter(todomvc.view.ToDoListContainerRenderer);
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
todomvc.view.ToDoListContainerRenderer.prototype.canDecorate = function(element) {
todomvc.view.ToDoListContainerRenderer.prototype.canDecorate =
function(element) {
return element.tagName == 'UL';
};
......@@ -30,7 +32,8 @@ todomvc.view.ToDoListContainerRenderer.prototype.canDecorate = function(element)
* @param {goog.ui.Container} container Container whose DOM is to be initialized
* as it enters the document.
*/
todomvc.view.ToDoListContainerRenderer.prototype.initializeDom = function(container) {
todomvc.view.ToDoListContainerRenderer.prototype.initializeDom =
function(container) {
var elem = (/**@type {!Element}*/ container.getElement());
// Set the ARIA role.
......
......@@ -3,20 +3,17 @@
/**
* A todo list item template
* @param content the label for this item
* @param checked whether the item is checked
*/
{template .toDoItem}
<li>
<div>
<div class="display">
<input class="check" type="checkbox" />
<div class="todo-content" style="cursor: pointer;">{$content}</div>
<span class="todo-destroy"></span>
<li>
<div class="view">
<input class="toggle" type="checkbox" {if $checked}checked{/if}>
<label>{$content}</label>
<button class="destroy"></button>
</div>
<div class="edit">
<input class="todo-input" type="text"/>
</div>
</div>
</li>
<input class="edit" value="Rule the web">
</li>
{/template}
/**
......@@ -24,9 +21,7 @@
* @param number the count of items
*/
{template .itemCount}
<span class="todo-count">
{call .itemCountInner data="all"/}
</span>
<span id="todo-count">{call .itemCountInner data="all"/}</span>
{/template}
/**
......@@ -34,7 +29,7 @@
* @param number the count of items
*/
{template .itemCountInner}
<span class="number">{$number}</span> <span class="word">{if $number > 1}items{else}item{/if}</span> left.
<strong>{$number}</strong> {if $number == 1}item{else}items{/if} left
{/template}
/**
......@@ -42,9 +37,7 @@
* @param number the count of items
*/
{template .clearCompleted}
<span class="todo-clear">
{call .clearCompletedInner data="all"/}
</span>
<button id="clear-completed">{call .clearCompletedInner data="all"/}</button>
{/template}
/**
......@@ -52,7 +45,5 @@
* @param number the count of items
*/
{template .clearCompletedInner}
<a href="#">
Clear <span class="number-done">{$number}</span> <span class="word-done">{if $number > 1}items{else}item{/if}</span>
</a>
Clear completed ({$number})
{/template}
\ No newline at end of file
{
"id" : "todomvc",
"inputs" : "js/main.js",
"inputs" : "js/app.js",
"paths" : "js/",
"output-wrapper" : "(function(){%output%})();",
"mode" : "ADVANCED",
......
File mode changed from 100755 to 100644
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment