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">
<ul id="todo-list">
</ul>
</div>
<div id="todo-stats">
</div>
</div>
</div>
<ul id="instructions">
<li>Click to edit a todo</li>
</ul>
<div id="credits">
Created by <a href="http://www.scottlogic.co.uk/blog/chris/">Chris Price</a>
</div>
<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>
</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>
</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
......@@ -2,51 +2,73 @@ 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) {
/**
* note the text associated with this item
* @private
* @type {!string}
*/
this.note_ = note;
/**
* is this item complete?
* @private
* @type {!boolean}
*/
this.done_ = opt_done || false;
todomvc.model.ToDoItem = function(note, opt_done, opt_id) {
/**
* note the text associated with this item
* @private
* @type {!string}
*/
this.note_ = note;
/**
* is this item complete?
* @private
* @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_;
return this.note_;
};
/**
* @return {!boolean} is this item complete?
*/
todomvc.model.ToDoItem.prototype.isDone = function() {
return this.done_;
return this.done_;
};
/**
* @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
* @param {!string} note the text associated with this item.
*/
todomvc.model.ToDoItem.prototype.setNote = function(note) {
this.note_ = note;
this.note_ = note;
};
/**
* @param {!boolean} done is this item complete?
*/
todomvc.model.ToDoItem.prototype.setDone = function(done) {
this.done_ = done;
};
\ No newline at end of file
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);
......@@ -6,14 +6,15 @@ goog.require('goog.ui.ControlRenderer');
/**
* A renderer for the clear completed control.
*
*
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
todomvc.view.ClearCompletedControlRenderer = function() {
goog.ui.ControlRenderer.call(this);
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,43 +23,47 @@ 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) {
var html = todomvc.view.clearCompleted({
number : control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
return element;
todomvc.view.ClearCompletedControlRenderer.prototype.createDom =
function(control) {
var html = todomvc.view.clearCompleted({
number: control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
return element;
};
/**
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
todomvc.view.ClearCompletedControlRenderer.prototype.canDecorate = function(element) {
return false;
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) {
element.innerHTML = todomvc.view.clearCompletedInner({
number : content
});
todomvc.view.ClearCompletedControlRenderer.prototype.setContent =
function(element, content) {
element.innerHTML = todomvc.view.clearCompletedInner({
number: content
});
};
/**
* Updates the appearance of the control in response to a state change.
*
*
* @param {goog.ui.Control} control Control instance to update.
* @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) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
}
todomvc.view.ClearCompletedControlRenderer.prototype.setState =
function(control, state, enable) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
}
};
......@@ -6,12 +6,12 @@ goog.require('goog.ui.ControlRenderer');
/**
* A renderer for the item count control.
*
*
* @constructor
* @extends {goog.ui.ControlRenderer}
*/
todomvc.view.ItemCountControlRenderer = function() {
goog.ui.ControlRenderer.call(this);
goog.ui.ControlRenderer.call(this);
};
goog.inherits(todomvc.view.ItemCountControlRenderer, goog.ui.ControlRenderer);
......@@ -23,42 +23,45 @@ goog.addSingletonGetter(todomvc.view.ItemCountControlRenderer);
* @return {Element} Root element for the control.
*/
todomvc.view.ItemCountControlRenderer.prototype.createDom = function(control) {
var html = todomvc.view.itemCount({
number : control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
return element;
var html = todomvc.view.itemCount({
number: control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
return element;
};
/**
* @param {Element} element Element to decorate.
* @return {boolean} Whether the renderer can decorate the element.
*/
todomvc.view.ItemCountControlRenderer.prototype.canDecorate = function(element) {
return false;
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) {
element.innerHTML = todomvc.view.itemCountInner({
number : content
});
todomvc.view.ItemCountControlRenderer.prototype.setContent =
function(element, content) {
element.innerHTML = todomvc.view.itemCountInner({
number: content
});
};
/**
* Updates the appearance of the control in response to a state change.
*
*
* @param {goog.ui.Control} control Control instance to update.
* @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) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
}
todomvc.view.ItemCountControlRenderer.prototype.setState =
function(control, state, enable) {
var element = control.getElement();
if (element) {
this.updateAriaState(element, state, enable);
}
};
......@@ -2,123 +2,149 @@ 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.
*
* @param {goog.dom.DomHelper=} opt_domHelper Optional DOM helper, used for document interaction.
* 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.
* @constructor
* @extends {goog.ui.Control}
*/
todomvc.view.ToDoItemControl = function(opt_domHelper) {
goog.ui.Control.call(this, "", todomvc.view.ToDoItemControlRenderer
.getInstance(), opt_domHelper);
goog.ui.Control.call(this, '', todomvc.view.ToDoItemControlRenderer
.getInstance(), opt_domHelper);
// enable CHECKED and SELECTED states
this.setSupportedState(goog.ui.Component.State.CHECKED, true);
this.setSupportedState(goog.ui.Component.State.SELECTED, true);
// disable auto handling of CHECKED and SELECTED states
this.setAutoStates(goog.ui.Component.State.CHECKED, false);
this.setAutoStates(goog.ui.Component.State.SELECTED, false);
// allow text selection within this control
this.setAllowTextSelection(true);
// enable CHECKED and SELECTED states
this.setSupportedState(goog.ui.Component.State.CHECKED, true);
this.setSupportedState(goog.ui.Component.State.SELECTED, true);
// disable auto handling of CHECKED and SELECTED states
this.setAutoStates(goog.ui.Component.State.CHECKED, false);
this.setAutoStates(goog.ui.Component.State.SELECTED, false);
// 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'
};
/**
* Configures the component after its DOM has been rendered, and sets up event
* handling. Overrides {@link goog.ui.Component#enterDocument}.
*
*
* @override
*/
todomvc.view.ToDoItemControl.prototype.enterDocument = function() {
todomvc.view.ToDoItemControl.superClass_.enterDocument.call(this);
// prevent clicking the checkbox (or anything within the root element)
// from having any default behaviour. This stops the checkbox being set
// by the browser.
this.getHandler().listen(this.getElement(), goog.events.EventType.CLICK,
function(e) {
e.preventDefault();
});
todomvc.view.ToDoItemControl.superClass_.enterDocument.call(this);
// prevent clicking the checkbox (or anything within the root element)
// from having any default behaviour. This stops the checkbox being set
// by the browser.
this.getHandler().listen(this.getElement(), goog.events.EventType.CLICK,
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_);
return (/**@type {todomvc.view.ToDoItemControlRenderer}*/ this.renderer_);
};
/**
* Specialised handling of mouse events when clicking on the checkbox, label,
* textbox or remove link.
*
*
* @param {goog.events.Event} e Mouse event to handle.
*/
todomvc.view.ToDoItemControl.prototype.handleMouseUp = function(e) {
todomvc.view.ToDoItemControl.superClass_.handleMouseUp.call(this, e);
if (this.isEnabled()) {
if (e.target === this.getRenderer().getCheckboxElement(
this.getElement())) {
this.setChecked(!this.isChecked());
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.EDIT);
} else if (e.target === this.getRenderer().getDestroyElement(
this.getElement())) {
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.DESTROY);
} else if (!this.isSelected()) {
this.setSelected(true);
}
}
todomvc.view.ToDoItemControl.superClass_.handleMouseUp.call(this, e);
if (this.isEnabled()) {
if (e.target === this.getRenderer().getCheckboxElement(
this.getElement())) {
this.setChecked(!this.isChecked());
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.EDIT);
} else if (e.target === this.getRenderer().getDestroyElement(
this.getElement())) {
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.DESTROY);
}
}
};
/**
* 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);
if (!focused && this.isSelected()) {
/**
* @type {Element}
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
this.setContent(inputElement.value);
this.setSelected(false);
this.dispatchEvent(todomvc.view.ToDoItemControl.EventType.EDIT);
}
todomvc.view.ToDoItemControl.superClass_.setFocused.call(this, focused);
if (!focused && this.isSelected()) {
/**
* @type {Element}
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
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);
if (selected) {
/**
* @type {Element}
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
inputElement.value = this.getContent();
inputElement.focus();
}
};
\ No newline at end of file
todomvc.view.ToDoItemControl.superClass_.setSelected.call(this, selected);
// populate the input box when selected
if (selected) {
/**
* @type {Element}
*/
var inputElement = this.getRenderer().getInputElement(
this.getElement());
inputElement.value = this.getContent();
inputElement.select();
}
};
......@@ -4,14 +4,14 @@ 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}
*/
todomvc.view.ToDoItemControlRenderer = function() {
goog.ui.ControlRenderer.call(this);
goog.ui.ControlRenderer.call(this);
};
goog.inherits(todomvc.view.ToDoItemControlRenderer, goog.ui.ControlRenderer);
......@@ -23,36 +23,40 @@ goog.addSingletonGetter(todomvc.view.ToDoItemControlRenderer);
* @return {Element} Root element for the control.
*/
todomvc.view.ToDoItemControlRenderer.prototype.createDom = function(control) {
var html = todomvc.view.toDoItem({
content : control.getContent()
});
var element = (/**@type {!Element}*/ goog.dom.htmlToDocumentFragment(html));
this.setAriaStates(control, element);
return element;
var html = todomvc.view.toDoItem({
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;
};
/**
* Updates the appearance of the control in response to a state change.
*
*
* @param {goog.ui.Control} control Control instance to update.
* @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) {
var element = control.getElement();
if (element) {
switch (state) {
case goog.ui.Component.State.CHECKED:
this.enableClassName(control, "done", enable);
this.getCheckboxElement(element).checked = enable;
break;
case goog.ui.Component.State.SELECTED:
this.enableClassName(control, "editing", enable);
break;
}
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.getCheckboxElement(element).checked = enable;
break;
case goog.ui.Component.State.SELECTED:
this.enableClassName(control, 'editing', enable);
break;
}
this.updateAriaState(element, state, enable);
}
this.updateAriaState(element, state, enable);
}
};
/**
......@@ -63,67 +67,68 @@ 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());
};
/**
* Takes the control's root element and returns the display element
*
*
* @param {Element} element Root element of the control whose display element is
* to be returned.
* @return {Element} The control's display element.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getDisplayElement = function(
element) {
return element ? element.childNodes[0].childNodes[0] : null;
element) {
return element ? element.childNodes[0] : null;
};
/**
* Takes the control's root element and returns the parent element of the
* control's contents.
*
*
* @param {Element} element Root element of the control whose content element is
* to be returned.
* @return {Element} The control's content element.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getContentElement = function(
element) {
return element ? this.getDisplayElement(element).childNodes[1] : null;
element) {
return element ? this.getDisplayElement(element).childNodes[1] : null;
};
/**
* Takes the control's root element and returns the checkbox element
*
*
* @param {Element} element Root element of the control whose checkbox element
* is to be returned.
* @return {Element} The control's checkbox element.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getCheckboxElement = function(
element) {
return element ? this.getDisplayElement(element).childNodes[0] : null;
element) {
return element ? this.getDisplayElement(element).childNodes[0] : null;
};
/**
* Takes the control's root element and returns the destroy element
*
*
* @param {Element} element Root element of the control whose destroy element is
* to be returned.
* @return {Element} The control's destroy element.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getDestroyElement = function(
element) {
return element ? this.getDisplayElement(element).childNodes[2] : null;
element) {
return element ? this.getDisplayElement(element).childNodes[2] : null;
};
/**
* Takes the control's root element and returns the input element
*
*
* @param {Element} element Root element of the control whose input element is
* to be returned.
* @return {Element} The control's input element.
*/
todomvc.view.ToDoItemControlRenderer.prototype.getInputElement = function(
element) {
return element ? element.childNodes[0].childNodes[1].childNodes[0] : null;
element) {
return element ? element.childNodes[1] : null;
};
......@@ -5,31 +5,33 @@ goog.require('goog.ui.Container');
goog.require('todomvc.view.ToDoListContainerRenderer');
/**
* 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.
* 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.
* @constructor
* @extends {goog.ui.Container}
*/
todomvc.view.ToDoListContainer = function(opt_domHelper) {
goog.ui.Container
.call(this, goog.ui.Container.Orientation.VERTICAL,
todomvc.view.ToDoListContainerRenderer.getInstance(),
opt_domHelper);
goog.ui.Container
.call(this, goog.ui.Container.Orientation.VERTICAL,
todomvc.view.ToDoListContainerRenderer.getInstance(),
opt_domHelper);
// allow focus on children
this.setFocusable(false);
this.setFocusableChildrenAllowed(true);
// allow focus on children
this.setFocusable(false);
this.setFocusableChildrenAllowed(true);
};
goog.inherits(todomvc.view.ToDoListContainer, goog.ui.Container);
/**
* Override this method to allow text selection in children.
*
*
* @param {goog.events.BrowserEvent} e Mousedown event to handle.
*/
todomvc.view.ToDoListContainer.prototype.handleMouseDown = function(e) {
if (this.enabled_) {
this.setMouseButtonPressed(true);
}
};
\ No newline at end of file
if (this.enabled_) {
this.setMouseButtonPressed(true);
}
};
......@@ -5,37 +5,40 @@ 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}
*/
todomvc.view.ToDoListContainerRenderer = function() {
goog.ui.ContainerRenderer.call(this);
goog.ui.ContainerRenderer.call(this);
};
goog.inherits(todomvc.view.ToDoListContainerRenderer,
goog.ui.ContainerRenderer);
goog.ui.ContainerRenderer);
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) {
return element.tagName == 'UL';
todomvc.view.ToDoListContainerRenderer.prototype.canDecorate =
function(element) {
return element.tagName == 'UL';
};
/**
* Override this method to allow text selection in children
*
*
* @param {goog.ui.Container} container Container whose DOM is to be initialized
* as it enters the document.
*/
todomvc.view.ToDoListContainerRenderer.prototype.initializeDom = function(container) {
var elem = (/**@type {!Element}*/ container.getElement());
todomvc.view.ToDoListContainerRenderer.prototype.initializeDom =
function(container) {
var elem = (/**@type {!Element}*/ container.getElement());
// Set the ARIA role.
var ariaRole = this.getAriaRole();
if (ariaRole) {
goog.dom.a11y.setRole(elem, ariaRole);
}
};
\ No newline at end of file
// Set the ARIA role.
var ariaRole = this.getAriaRole();
if (ariaRole) {
goog.dom.a11y.setRole(elem, ariaRole);
}
};
......@@ -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>
</div>
<div class="edit">
<input class="todo-input" type="text"/>
</div>
</div>
</li>
<li>
<div class="view">
<input class="toggle" type="checkbox" {if $checked}checked{/if}>
<label>{$content}</label>
<button class="destroy"></button>
</div>
<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