Commit 03f8af01 authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #1101 from asudoh/dojo110

Dojo example update with Dojo 1.10, fixes #1088.
parents 940324e8 cd245c56
bower_components
html-report
node_modules
out
js/lib/*
!js/lib/dojo
js/lib/dojo/*
!js/lib/dojo/dojo.js
{
"name": "todmvc-dojo",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.3.0"
}
"name": "todomvc-dojo",
"dependencies": {
"todomvc-common": "~0.3.0"
},
"devDependencies": {
"dojo": "~1.10.0",
"dijit": "~1.10.0",
"dojox": "~1.10.0",
"util": "dojo/util#~1.10.0"
},
"private": true
}
#clear-completed, #footer, #main,
.plural {
display: none;
}
#todoapp.todos_selected #clear-completed,
#todoapp.todos_present #footer,
#todoapp.todos_present #main,
.multiple .plural {
display: inherit;
}
#todo-list li.hidden {
display: none;
}
#todo-list li .toggle.dijitChecked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
/* When checkbox is selected, score through todo item content */
.dijitCheckBoxChecked + .todo-content {
color: #666;
text-decoration: line-through;
}
/* When checkbox is selected, score through todo item content after an edit */
.dijitCheckBoxChecked + .dijitInline + .todo-content {
color: #666;
text-decoration: line-through;
}
#todo-list .dijitCheckBoxInput {
opacity: 0;
position: absolute;
top: 14px;
z-index: 10;
}
/** Match up inline edit box with styling */
.dijitInputInner {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todo-list li .dijitInputInner {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
z-index: 10;
}
/** Ugh, force override of edit container margin */
#todo-list li span.dijitInline {
margin: 0 !important;
}
/**
* Inline edit box doesn't provide indication via class names
* when a box is 'live'. Style values are set manually. Use the
* opacity change as indicator... :( */
.inline_edit[style~='0;'] ~ .toggle {
visibility: hidden !important;
}
.dijitOffScreen { /* For 1.8 in-line edit box */
position: absolute !important;
left: 50% !important;
top: -10000px !important;
}
<!doctype html>
<html lang="en" data-framework="dojo">
<head>
<meta charset="utf-8">
<title>Dojo • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section id="todoapp" data-dojo-type="todo.app">
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://jamesthom.as/">James Thomas</a> and <a href="https://github.com/edchat">Ed Chatelain</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/todomvc-common/base.js"></script>
<script data-dojo-config="async:true, parseOnLoad:true, locale:'en', paths:{'todo':'../todo/'}, deps:['dojo/parser', 'todo/app']" src="js/lib/dojo/dojo.js"></script>
</body>
</html>
......@@ -3,30 +3,71 @@
<head>
<meta charset="utf-8">
<title>Dojo • TodoMVC</title>
<style>
[hidden="true"] {
display: none;
}
</style>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section id="todoapp" data-dojo-type="todo/app18"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://jamesthom.as/">James Thomas</a> and <a href="https://github.com/edchat">Ed Chatelain</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/todomvc-common/base.js"></script>
<script>
require = {
async: true,
parseOnLoad: true,
locale: 'en',
paths: {
dijit: '../dijit-1.8'
},
deps: ['dojo/parser', 'dojo/domReady!'],
mvc: { debugBindings: true }
};
<script type="dojo/require">
at: 'dojox/mvc/at'
</script>
<script src="js/lib/dojo-1.8/dojo.js"></script>
<script id="item-template" type="dojox/mvc/InlineTemplate">
<li>
<div class="view">
<input class="toggle" type="checkbox" data-dojo-type="dojox/mvc/Element" data-dojo-props="checked: at('rel:', 'completed')" data-dojo-attach-event="change: toggleCompleted">
<label data-dojo-type="dojox/mvc/Element" data-dojo-props="innerText: at('rel:', 'title')" data-dojo-attach-event="dblclick: editTodo"></label>
<button class="destroy" data-dojo-attach-event="click: removeTodo"></button>
</div>
<form data-dojo-attach-event="submit: saveEdits">
<input class="edit" data-dojo-type="dojox/mvc/Element" data-dojo-mixins="todo/widgets/TodoEscape,todo/widgets/TodoFocus" data-dojo-props="_setDisabledAttr: 'domNode', value: at('rel:', 'title'), shouldGetFocus: at('rel:', 'isEditing'), disabled: at('rel:todosCtrl', 'saving')" data-dojo-attach-event="blur: invokeSaveEdits, escape: revertEdits">
</form>
</li>
</script>
<section id="todoapp" data-dojo-type="todo/widgets/Todos">
<script type="dojox/mvc/InlineTemplate">
<section>
<header id="header">
<h1>todos</h1>
<form id="todo-form" data-dojo-attach-event="submit: addTodo">
<input id="new-todo" placeholder="What needs to be done?" data-dojo-type="dojox/mvc/Element" data-dojo-props="_setDisabledAttr: 'domNode', value: at(this, 'newTodo'), disabled: at(this, 'saving')" autofocus>
</form>
</header>
<section id="main" data-dojo-type="dijit/_WidgetBase" data-dojo-props="_setHiddenAttr: '', hidden: at(this.get('todos'), 'length').transform(this.emptyConverter)">
<input id="toggle-all" type="checkbox" data-dojo-type="dojox/mvc/Element" data-dojo-props="checked: at(this, 'areAllChecked')" data-dojo-attach-event="change: markAll">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"
data-dojo-type="dojox/mvc/WidgetList"
data-dojo-props="todosWidget: this, children: at(this, 'filteredTodos'), partialRebuild: true, templateString: require('dojo/dom').byId('item-template').innerHTML"
data-mvc-child-type="todo/widgets/Todo"
data-mvc-child-mixins="todo/widgets/CSSToggleWidget"
data-mvc-child-props="todosWidget: this.parent.todosWidget, _setCompletedAttr: {type: 'classExists'}, _setIsEditingAttr: {type: 'classExists', className: 'editing'}, completed: at(this.target, 'completed'), isEditing: at(this.target, 'isEditing')">
</section>
<footer id="footer" data-dojo-type="dijit/_WidgetBase" data-dojo-props="_setHiddenAttr: '', hidden: at(this.get('todos'), 'length').transform(this.emptyConverter)">
<span id="todo-count"><strong data-dojo-type="dojox/mvc/Element" data-dojo-props="innerText: at(this, 'remainingCount')"></strong>
<span data-dojo-type="dojox/mvc/Element" data-dojo-props="innerText: at(this, 'remainingCount').transform(this.pluralizeConverter), one: 'item left', other: 'items left'"></span>
</span>
<ul id="filters">
<li>
<a data-dojo-type="todo/widgets/CSSToggleWidget" data-dojo-props="_setSelectedAttr: {type: 'classExists'}, selected: at(this, 'status').transform(this.statusConverter), statusForElem: ''" href="#/">All</a>
</li>
<li>
<a data-dojo-type="todo/widgets/CSSToggleWidget" data-dojo-props="_setSelectedAttr: {type: 'classExists'}, selected: at(this, 'status').transform(this.statusConverter), statusForElem: 'active'" href="#/active">Active</a>
</li>
<li>
<a data-dojo-type="todo/widgets/CSSToggleWidget" data-dojo-props="_setSelectedAttr: {type: 'classExists'}, selected: at(this, 'status').transform(this.statusConverter), statusForElem: 'completed'" href="#/completed">Completed</a>
</li>
</ul>
<button id="clear-completed" data-dojo-type="dijit/_WidgetBase" data-dojo-attach-event="click: clearCompletedTodos" data-dojo-props="_setHiddenAttr: '', hidden: at(this, 'completedCount').transform(this.emptyConverter)">Clear completed (<span data-dojo-type="dojox/mvc/Element" data-dojo-props="innerText: at(this, 'completedCount')"></span>)</button>
</footer>
</section>
</script>
</section>
<script src="js/main.js"></script>
<script src="js/lib/dojo/dojo.js"></script>
<!-- Use below instead of above line to use non-built version of Dojo components. -->
<!-- <script src="bower_components/dojo/dojo.js"></script> -->
</body>
</html>
define("dijit/nls/common", { root:
//begin v1.x content
({
buttonOk: "OK",
buttonCancel: "Cancel",
buttonSave: "Save",
itemClose: "Close"
})
//end v1.x content
,
"zh": true,
"zh-tw": true,
"tr": true,
"th": true,
"sv": true,
"sl": true,
"sk": true,
"ru": true,
"ro": true,
"pt": true,
"pt-pt": true,
"pl": true,
"nl": true,
"nb": true,
"ko": true,
"kk": true,
"ja": true,
"it": true,
"hu": true,
"hr": true,
"he": true,
"fr": true,
"fi": true,
"es": true,
"el": true,
"de": true,
"da": true,
"cs": true,
"ca": true,
"az": true,
"ar": true
});
define(
//begin v1.x content
{
"decimal": ".",
"group": ",",
"list": ";",
"percentSign": "%",
"plusSign": "+",
"minusSign": "-",
"exponential": "E",
"perMille": "",
"infinity": "",
"nan": "NaN",
"decimalFormat": "#,##0.###",
"decimalFormat-short": "000T",
"scientificFormat": "#E0",
"percentFormat": "#,##0%",
"currencyFormat": "¤#,##0.00;(¤#,##0.00)"
}
//end v1.x content
);
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
define('dojo/nls/dojo_ROOT',{
'dijit/nls/common':{"buttonOk":"OK","buttonCancel":"Cancel","buttonSave":"Save","itemClose":"Close"}
});
\ No newline at end of file
define(
//begin v1.x content
{
"group": ",",
"percentSign": "%",
"exponential": "E",
"scientificFormat": "#E0",
"percentFormat": "#,##0%",
"list": ";",
"infinity": "",
"patternDigit": "#",
"minusSign": "-",
"decimal": ".",
"nan": "NaN",
"nativeZeroDigit": "0",
"perMille": "",
"decimalFormat": "#,##0.###",
"currencyFormat": "¤#,##0.00;(¤#,##0.00)",
"plusSign": "+",
"decimalFormat-short": "000T"
}
//end v1.x content
);
This source diff could not be displayed because it is too large. You can view the blob instead.
(function (global) {
'use strict';
global.require = {
async: true,
baseUrl: '.',
callback: function (parser) {
parser.parse();
},
deps: ['dojo/parser'],
packages: [
{
name: 'dojo',
location: './bower_components/dojo'
},
{
name: 'dijit',
location: './bower_components/dijit'
},
{
name: 'dojox',
location: './bower_components/dojox'
},
{
name: 'todo',
location: './js/todo'
}
],
map: {
// TodoMVC application does not use template from file system
'dijit/_TemplatedMixin': {
'dojo/cache': 'todo/empty'
}
},
parseOnLoad: false
};
})(this);
define([
'dojo/_base/declare',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'dojox/mvc/WidgetList',
'dojox/mvc/_InlineTemplateMixin',
'todo/CssToggleWidget',
'todo/ctrl/_HashCompletedMixin'
], function(declare, _TemplatedMixin, _WidgetsInTemplateMixin, WidgetList, _InlineTemplateMixin, CssToggleWidget, _HashCompletedMixin){
return declare([WidgetList, _InlineTemplateMixin], {
childClz: declare([CssToggleWidget, _TemplatedMixin, _WidgetsInTemplateMixin, _HashCompletedMixin], {
_setCompletedAttr: { type: 'classExists', className: 'completed' },
_setHiddenAttr: { type: 'classExists', className: 'hidden' },
onRemoveClick: function () {
this.parent.listCtrl.removeItem(this.uniqueId);
this.destroyRecursive();
},
onEditBoxChange: function () {
if (!this.editBox.value) {
this.parent.listCtrl.removeItem(this.uniqueId);
}
}
})
});
});
<section>
<header id="header">
<h1>todos</h1>
<input id="new-todo" data-dojo-attach-event="onkeypress:onKeyPress" placeholder="What needs to be done?" type="text" autofocus>
<span class="ui-tooltip-top" style="display:none;">Press Enter to save this task</span>
</header>
<section id="main">
<input id="toggle-all" data-dojo-attach-point="mark_all" data-dojo-attach-event="onclick:onMarkAll" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-dojo-attach-point="todo_list" data-dojo-type="dojox.mvc.Repeat" data-dojo-props="ref: this.model.todos, exprchar: '#'">
<li data-dojo-type="dojox.mvc.Group" data-dojo-props="ref: '#{this.index}'">
<div class="view">
<label class="dijitInline inline_edit" data-dojo-type="todo.form.InlineEditBox"
data-dojo-props='ref: "title", editor:"dijit.form.TextBox", autosave:true'></label>
<input class="toggle" type="checkbox" data-dojo-type="todo.form.CheckBox" data-dojo-props='ref: "completed"'>
<button class="destroy" data-model-id="#{this.index}">
</button>
</div>
</li>
</ul>
</section>
<footer id="footer" data-dojo-attach-point="todo_stats">
<span id="todo-count">
<strong>
<span data-dojo-type="dojox.mvc.Output" data-dojo-props="ref: this.model.incomplete" class="number"></span>
</strong>
<span class="word">item<span class="plural">s</span></span> left
</span>
<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>
<button id="clear-completed" data-dojo-attach-event="onclick:removeCompletedItems">
Clear completed (<span class="number-done" data-dojo-type="dojox.mvc.Output" data-dojo-props="ref: this.model.complete"></span>)
</button>
</footer>
</section>
/**
* Original source from https://gist.github.com/880822
* Converted to AMD-baseless format
*/
define(["dojo/_base/declare",
// Parent classes
"dijit/_WidgetBase", "dijit/_TemplatedMixin", "dijit/_WidgetsInTemplateMixin",
// General application modules
"dojo/_base/lang", "dojo/_base/event", "dojo/on", "dojo/dom-class", "dojo/dom-attr", "dojo/query", "dojo/string",
"dijit/_base/manager", "dojo/keys", "dojox/mvc", "dojo/hash", "dojo/_base/connect", "todo/model/TodoModel",
// Widget template
"dojo/text!./app.html",
// Template Widgets
"todo/form/InlineEditBox", "todo/form/CheckBox", "dojox/mvc/Group", "dojox/mvc/Repeat", "dojox/mvc/Output"],
function(declare, _WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, lang, _event, on, domClass, domAttr,
query, str, manager, keys, mvc, hash, connect, TodoModel, template) {
return declare("todo.app", [_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
/** Widget template HTML string */
templateString: template,
/** Hash state constants */
ACTIVE: "/active",
COMPLETED: "/completed",
constructor: function () {
/**
* Create new application Model class, this will be used to bind
* the UI elements to the data store items. Pre-populate model with
* items from localStorage if they exist...
*/
this.model = new TodoModel();
/**
* The method below set up a function binding to the composite (complete & incomplete)
* model attributes. These values are used to append CSS classes to dynamically show & hide
* the "stats" elements when the model is non-empty and has some completed items. Whenever
* the values below are updated, the function will be executed.
*/
mvc.bindInputs([this.model.complete, this.model.incomplete], lang.hitch(this, "onItemStatusUpdate"));
/**
* Hook into unload event to trigger persisting
* of the current model contents into the localStorage
* backed data store.
*/
window.onbeforeunload = lang.hitch(this, function () {
this.model.commit();
});
/** Connect to changes to the URI hash */
connect.subscribe("/dojo/hashchange", this, "onHashChange");
},
/**
* Listen for item remove events from the using event delegation,
* we don't have to attach to each item. Also, ensure todo-stats
* have the correct initial CSS classes given the starting model
* contents.
*/
postCreate: function () {
on(this.domNode, ".destroy:click", lang.hitch(this, "onRemove"));
on(this.domNode, ".view:dblclick", lang.hitch(this, "onEdit"));
this.onItemStatusUpdate();
},
/**
* Ensure application state reflects current
* hash value after rendering model in the view.
*/
startup: function () {
this.inherited(arguments);
this.onHashChange(hash());
},
/**
* Remove all items that have been completed from
* model. We have to individually check each todo
* item, removing if true.
*/
removeCompletedItems: function () {
var len = this.model.todos.length, idx = 0;
/**
* Removing array indices from a Dojo MVC Model
* array left-shifts the remaining items. When
* we find an item to remove, don't increment the
* index and, instead, decrement the total item count.
*/
while (idx < len) {
if (this.model.todos[idx].completed.value) {
this.model.todos.remove(idx);
len--;
continue;
}
idx++;
}
},
/**
* Add new a new todo item as the last element
* in the parent model.
*/
addToModel: function (content, completed) {
var insert = mvc.newStatefulModel({
data: {title: content, completed: completed}
});
this.model.todos.add(this.model.todos.length, insert);
},
/**
* Adjust CSS classes on todo-stats element based upon whether
* we a number of completed and incomplete todo items.
* Also verify state of the "Mark All" box.
*/
onItemStatusUpdate: function () {
var completed = this.model.complete.get("value"),
length = this.model.todos.get("length");
domClass.toggle(this.domNode, "todos_selected", completed > 0);
domClass.toggle(this.domNode, "multiple", completed > 1);
domClass.toggle(this.domNode, "todos_present", length);
domAttr.set(this.mark_all, "checked", length && length === completed);
setTimeout(lang.hitch(this, "onHashChange", hash()));
},
/**
* Event fired when user selects the "Mark All" checkbox.
* Update selection state of all the todos based upon current
* checked value.
*/
onMarkAll: function () {
var checked = this.mark_all.checked;
for(var i = 0, len = this.model.todos.length; i < len; i++) {
this.model.todos[i].completed.set("value", checked);
}
},
/**
* Handle key press events for the todo input
* field. If user has pressed enter, add current
* text value as new todo item in the model.
*/
onKeyPress: function (event) {
if (event.keyCode !== keys.ENTER ||
!str.trim(event.target.value).length) {
return;
}
this.addToModel(event.target.value, false);
event.target.value = "";
_event.stop(event);
},
/**
* Event handler when user has clicked to
* remove a todo item, just remove it from the
* model using the item identifier.
*/
onRemove: function (event) {
this.model.todos.remove(domAttr.get(event.target, "data-model-id"));
},
/**
* Whenever the user double clicks the item label,
* set inline edit box to true.
*/
onEdit: function (event) {
query(".inline_edit", event.target).forEach(function (inline_edit) {
manager.byNode(inline_edit).edit();
});
},
/**
* When the URI's hash value changes, modify the
* displayed list items to show either completed,
* remaining or all tasks.
* Also highlight currently selected link value.
*/
onHashChange: function (hash) {
var showIfDone = (hash === this.COMPLETED ? false :
(hash === this.ACTIVE? true : null));
query("#todo-list > li").forEach(lang.hitch(this, function (item, idx) {
var done = this.model.todos[idx].completed.get("value");
domClass.toggle(item, "hidden", done === showIfDone);
}));
/** Normalise hash value to match link hrefs */
hash = "#" + (showIfDone !== null ? hash : "/");
query("#filters a").forEach(lang.hitch(this, function (link) {
domClass.toggle(link, "selected", domAttr.get(link, "href") === hash);
}));
}
});
});
<section>
<script type="dojo/require">
at: "dojox/mvc/at",
SimpleTodoModel: "todo/model/SimpleTodoModel",
HashSelectedConverter: "todo/misc/HashSelectedConverter",
LessThanOrEqualToConverter: "todo/misc/LessThanOrEqualToConverter"
</script>
<span data-dojo-id="${id}_routeCtl" data-dojo-type="todo/ctrl/RouteController"></span>
<span data-dojo-id="${id}_localStorage" data-dojo-type="todo/store/LocalStorage"></span>
<span data-dojo-id="${id}_ctrl"
data-dojo-type="todo/ctrl/TodoRefController"
data-dojo-props="defaultId: 'todos-dojo', store: ${id}_localStorage, modelClass: SimpleTodoModel, complete: at('widget:${id}', 'complete')"></span>
<span data-dojo-id="${id}_listCtrl"
data-dojo-type="todo/ctrl/TodoListRefController"
data-dojo-props="model: at(${id}_ctrl, 'todos'), length: at('widget:${id}', 'present').direction(at.to)"></span>
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" type="text" autofocus
data-dojo-attach-event="onkeypress: onKeyPressNewItem">
<span class="ui-tooltip-top" style="display:none;">Press Enter to save this task</span>
</header>
<section id="main">
<input id="toggle-all" type="checkbox"
data-dojo-type="dijit/form/CheckBox"
data-dojo-props="checked: at(${id}_ctrl, 'incomplete').transform(LessThanOrEqualToConverter),
onClick: function(e){ ${id}_listCtrl.markAll(e.target.checked); }">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"
data-dojo-type="todo/TodoList"
data-dojo-props="children: at(${id}_listCtrl, 'model'), partialRebuild: true, listCtrl: ${id}_listCtrl"
data-mvc-child-props="uniqueId: at(this.target, 'uniqueId'), completed: at(this.target, 'completed'), hash: at(${id}_routeCtl, 'hash')">
<script type="dojox/mvc/InlineTemplate">
<li>
<div class="view">
<label class="dijitInline inline_edit"
data-dojo-attach-point="editBox"
data-dojo-attach-event="onChange: onEditBoxChange"
data-dojo-type="todo/form/InlineEditBox"
data-dojo-props="value: at('rel:', 'title'), editor: 'dijit/form/TextBox', autosave: true"></label>
<input class="toggle" type="checkbox"
data-dojo-type="dijit/form/CheckBox"
data-dojo-props="checked: at('rel:', 'completed')">
<button class="destroy" data-dojo-attach-event="onclick: onRemoveClick"></button>
</div>
</li>
</script>
</ul>
</section>
<footer id="footer">
<span id="todo-count">
<strong>
<span class="number"
data-dojo-type="dijit/_WidgetBase"
data-dojo-props="_setValueAttr: {type: 'innerText', node: 'domNode'}, value: at(${id}_ctrl, 'incomplete')"></span>
</strong>
<span class="word">item<!--
--><span data-dojo-type="todo/CssToggleWidget"
data-dojo-props="_setSingleAttr: {type: 'classExists', className: 'plural'},
constraints: {lessThanOrEqualTo: 1},
single: at(${id}_ctrl, 'incomplete').transform(LessThanOrEqualToConverter)">s</span></span>
left
</span>
<ul id="filters">
<li>
<a href="#/"
data-dojo-type="todo/CssToggleWidget"
data-dojo-props="_setSelectedAttr: {type: 'classExists', className: 'selected'},
selected: at(${id}_routeCtl, 'hash').transform(HashSelectedConverter)">All</a>
</li>
<li>
<a href="#/active"
data-dojo-type="todo/CssToggleWidget"
data-dojo-props="_setSelectedAttr: {type: 'classExists', className: 'selected'},
selected: at(${id}_routeCtl, 'hash').transform(HashSelectedConverter)">Active</a>
</li>
<li>
<a href="#/completed"
data-dojo-type="todo/CssToggleWidget"
data-dojo-props="_setSelectedAttr: {type: 'classExists', className: 'selected'},
selected: at(${id}_routeCtl, 'hash').transform(HashSelectedConverter)">Completed</a>
</li>
</ul>
<button id="clear-completed" onclick="${id}_listCtrl.removeCompletedItems();">
Clear completed (<!--
--><span class="number-done"
data-dojo-type="dijit/_WidgetBase"
data-dojo-props="_setValueAttr: {type: 'innerText', node:
'domNode'}, value: at(${id}_ctrl, 'complete')"></span><!--
-->)</button>
</footer>
</section>
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/_base/unload",
"dojo/keys",
"dojo/string",
"dijit/_TemplatedMixin",
"dijit/_WidgetsInTemplateMixin",
"todo/CssToggleWidget",
"dojo/text!./app18.html",
// Below modules are referred in template.
// dijit/_WidgetsInTemplateMixin requires all modules referred in template to have been loaded before it's instantiated.
"dijit/form/CheckBox",
"dijit/form/TextBox",
"dojox/mvc/at",
"todo/TodoList",
"todo/ctrl/RouteController",
"todo/ctrl/TodoListRefController",
"todo/ctrl/TodoRefController",
"todo/form/InlineEditBox",
"todo/misc/HashSelectedConverter",
"todo/misc/LessThanOrEqualToConverter",
"todo/model/SimpleTodoModel",
"todo/store/LocalStorage"
], function(declare, lang, unload, keys, string, _TemplatedMixin, _WidgetsInTemplateMixin, CssToggleWidget, template){
return declare([CssToggleWidget, _TemplatedMixin, _WidgetsInTemplateMixin], {
// summary:
// A widgets-in-template widget that composes the application UI of TodoMVC (Dojo 1.8 version).
// Also, this class inherits todo/CssToggleWidget so that it can react to change in "present"/"complete" attributes and add/remove CSS class to the root DOM node of this widget.
// templateString: String
// The HTML of widget template.
templateString: template,
// _setPresentAttr: Object
// A object used by todo/CssToggleWidget to reflect true/false state of "present" attribute to existence of "todos_present" CSS class in this widget's root DOM.
_setPresentAttr: {type: "classExists", className: "todos_present"},
// _setCompleteAttr: Object
// A object used by todo/CssToggleWidget to reflect true/false state of "complete" attribute to existence of "todos_selected" CSS class in this widget's root DOM.
_setCompleteAttr: {type: "classExists", className: "todos_selected"},
startup: function(){
// summary:
// A function called after the DOM fragment declaring this controller is added to the document.
// See documentation for dijit/_WidgetBase.startup() for more details.
var _self = this;
this.inherited(arguments);
unload.addOnUnload(function(){
_self.destroy(); // When this page is being unloaded, call destroy callbacks of inner-widgets to let them clean up
});
},
onKeyPressNewItem: function(/*DOMEvent*/ event){
// summary:
// Handle key press events for the input field for new todo.
// description:
// If user has pressed enter, add current text value as new todo item in the model.
if(event.keyCode !== keys.ENTER || !string.trim(event.target.value).length){
return; // If the key is not Enter, or the input field is empty, bail
}
lang.getObject(this.id + "_listCtrl").addItem(event.target.value); // Add a todo item with the given text
event.target.value = ""; // Clear the input field
event.preventDefault();
event.stopPropagation();
}
});
});
// TODO: Remove this file once Dojo provides this feature out-of-the-box.
define([
'dojo/_base/array',
'dojo/has'
], function (array, has) {
'use strict';
var arrayProto = Array.prototype;
has.add('object-is-api', typeof Object.is === 'function');
var areSameValues = has('object-is-api') ? Object.is : function (lhs, rhs) {
return lhs === rhs && (lhs !== 0 || 1 / lhs === 1 / rhs) || lhs !== lhs && rhs !== rhs;
};
function watch(o, prop, callback) {
var hWatch;
if (o && typeof o.watch === 'function') {
hWatch = o.watch(prop, function (name, old, current) {
if (!areSameValues(old, current)) {
callback(current, old);
}
});
} else {
console.log('Attempt to observe non-stateful ' + o + ' with ' + prop + '. Observation not happening.');
}
return {
remove: function () {
if (hWatch) {
hWatch.remove();
}
}
};
}
function getProps(list) {
return array.map(list, function (p) {
return p.each ?
array.map(p.target, function (entry) {
return entry.get ? entry.get(p.targetProp) : entry[p.targetProp];
}) :
p.target.get ? p.target.get(p.targetProp) :
p.target[p.targetProp];
});
}
function removeHandles(handles) {
var h = null;
while ((h = handles.shift())) {
h.remove();
}
}
/**
* Returns a pointer to a dojo/Stateful property that are computed with other dojo/Stateful properties.
* @function computed
* @param {dojo/Stateful} target dojo/Stateful where the property is in.
* @param {string} targetProp The property name.
* @param {Function} compute
* The function, which takes dependent dojo/Stateful property values as the arguments,
* and returns the computed value.
* @param {...dojox/mvc/at} var_args The dojo/Stateful properties this computed property depends on.
* @returns {Object} The handle to clean up the computed property created. To clean it up, call its `remove()` method.
* @example
* <caption>
* If stateful.first is "John" and stateful.last is "Doe", stateful.name becomes "John Doe".
* </caption>
* computed(stateful, 'name', function(first, last){
* return first + ' ' + last;
* }, at(stateful, 'first'), at(stateful, 'last'));
* @example
* <caption>
* If names is an array of objects with name property,
* stateful.totalNameLength becomes the sum of length of name property of each array item.
* </caption>
* computed(stateful, 'totalNameLength', function(names){
* var total = 0;
* array.forEach(names, function(name){
* total += name.length;
* });
* return total;
* }, lang.mixin(at(names, 'name'), {each: true}));
*/
return function (target, targetProp, compute) {
function applyComputed(data) {
var result;
var hasResult;
try {
result = compute.apply(target, data);
hasResult = true;
} catch (e) {
console.error('Error during computed property callback: ' + (e && e.stack || e));
}
if (hasResult) {
if (typeof target.set === 'function') {
target.set(targetProp, result);
} else {
target[targetProp] = result;
}
}
}
if (target === null || target === undefined) {
throw new Error('Computed property cannot be applied to null.');
}
if (targetProp === '*') {
throw new Error('Wildcard property cannot be used for computed properties.');
}
var deps = arrayProto.slice.call(arguments, 3);
var hDep = array.map(deps, function (dep, index) {
function observeEntry(entry) {
return watch(entry, dep.targetProp, function () {
applyComputed(getProps(deps));
});
}
if (dep.targetProp === '*') {
throw new Error('Wildcard property cannot be used for computed properties.');
} else if (dep.each) {
var hArray;
var hEntry = array.map(dep.target, observeEntry);
if (dep.target && typeof dep.target.watchElements === 'function') {
hArray = dep.target.watchElements(function (idx, removals, adds) {
removeHandles(arrayProto.splice.apply(hEntry, [idx, removals.length].concat(array.map(adds, observeEntry))));
applyComputed(getProps(deps));
});
} else {
console.log('Attempt to observe non-stateful-array ' + dep.target + '. Observation not happening.');
}
return {
remove: function () {
if (hArray) {
hArray.remove();
}
removeHandles(hEntry);
}
};
} else {
return watch(dep.target, dep.targetProp, function (current) {
var list = [];
arrayProto.push.apply(list, getProps(deps.slice(0, index)));
list.push(current);
arrayProto.push.apply(list, getProps(deps.slice(index + 1)));
applyComputed(list);
});
}
});
applyComputed(getProps(deps));
return {
remove: function () {
removeHandles(hDep);
}
};
};
});
define([
"dojo/_base/declare",
"dojo/router",
"dijit/Destroyable",
"dojox/mvc/_Controller"
], function(declare, router, Destroyable, _Controller){
return declare([_Controller, Destroyable], {
// summary:
// A controller that maintains hash attribute in sync with location.hash.
// example:
// In this example, the text box is in sync with the URL hash.
// (The change in URL hash is reflected to the text box, and the edit in text box will be reflected to the URL hash)
// | <html>
// | <head>
// | <script src="/path/to/dojo-toolkit/dojo/dojo.js" type="text/javascript"
// | data-dojo-config="parseOnLoad: 1,
// | packages: [{name: 'todo', location: '/path/to/todo-package'}],
// | deps: ['dojo/parser', 'dojo/domReady!']"></script>
// | </head>
// | <body>
// | <script type="dojo/require">at: "dojox/mvc/at"</script>
// | <span id="routeCtrl" data-dojo-type="todo/ctrl/RouteController"></span>
// | <input data-dojo-type="dijit/form/TextBox"
// | data-dojo-props="value: at('widget:routeCtrl', 'hash')">
// | </body>
// | </html>
postscript: function(/*Object?*/ params, /*DOMNode?*/ srcNodeRef){
// summary:
// Kicks off instantiation of this controller, in a similar manner as dijit/_WidgetBase.postscript().
// params: Object?
// The optional parameters for this controller.
// srcNodeRef: DOMNode?
// The DOM node declaring this controller. Set if this controller is created via Dojo parser.
this.inherited(arguments);
srcNodeRef && srcNodeRef.setAttribute("widgetId", this.id); // If this is created via Dojo parser, set widgetId attribute so that destroyDescendants() of parent widget works
},
startup: function(){
// summary:
// A function called after the DOM fragment declaring this controller is added to the document, in a similar manner as dijit/_WidgetBase.startup().
var _self = this;
this.own(router.register(/.*/, function(e){ // Register a route handling callback for any route, make sure it's cleaned up upon this controller being destroyed
_self._set("hash", e.newPath); // Update hash property
}));
router.startup(); // Activate dojo/router
this.set("hash", router._currentPath); // Set the inital value of hash property
},
_setHashAttr: function(value){
// summary:
// Handler for calls to set("hash", val).
// description:
// If the new value is different from location.hash, updates location.hash.
if(this.hash != value){
router.go(value); // If the new value is different from location.hash, updates location.hash
}
this._set("hash", value); // Assign the new value to the property
}
});
})
define([
"dojo/_base/array",
"dojo/_base/declare",
"dojo/Stateful",
"dojox/mvc/ModelRefController"
], function(array, declare, Stateful, ModelRefController){
return declare(ModelRefController, {
// summary:
// Our custom controller that does:
//
// - Handle actions like adding/removing/marking
// - Provide references to the todo list in data model, whose data comes from above Dojo Object Store
//
// description:
// The todo list in the data model, which is based on dojox/mvc/StatefulArray, can be referred via this[this._refModelProp].
// Actions are implemented in the manner of manipulating array.
// The change will automatically be reflected to the UI via the notification system of dojox/mvc/StatefulArray.
addItem: function(/*String*/ title){
// summary:
// Adds a todo item with the given title.
// title: String
// The title of todo item.
this[this._refModelProp].push(new Stateful({title: title, completed: false}));
},
removeItem: function(/*String*/ uniqueId){
// summary:
// Removes a todo item having the given unique ID.
// uniqueId: String
// The unique ID of the todo item to be removed.
var model = this[this._refModelProp],
indices = array.filter(array.map(model, function(item, idx){ return item.uniqueId == uniqueId ? idx : -1; }), function(idx){ return idx >= 0; }); // The array index of the todo item to bd removed
if(indices.length > 0){
model.splice(indices[0], 1);
}
},
removeCompletedItems: function(){
// summary:
// Removes todo items that have been marked as complete.
var model = this[this._refModelProp];
for(var i = model.length - 1; i >= 0; --i){
if(model[i].get("completed")){
model.splice(i, 1);
}
}
},
markAll: function(/*Boolean*/ markComplete){
// summary:
// Mark all todo items as complete or incomplete.
// markComplete: Boolean
// True to mark all todo items as complete. Otherwise to mark all todo items as incomplete.
array.forEach(this[this._refModelProp], function(item){
item.set("completed", markComplete);
});
}
});
});
define([
"dojo/_base/array",
"dojo/_base/declare",
"dojox/mvc/EditStoreRefController"
], function(array, declare, EditStoreRefController){
return declare(EditStoreRefController, {
// summary:
// Our custom controller that does:
//
// - Load and save data using a Dojo Object Store
// - Provide references to the data model, whose data comes from above Dojo Object Store
// defaultId: String
// The default ID to fetch data as this controller starts up.
defaultId: "",
// store: dojo/store
// Our custom Dojo Object Store, backed by localStorage.
// This will be used to read the initial items, if available, and persist the current items when the application finishes.
store: null,
// modelClass: todo/model/SimpleTodoModel
// The class of our data model, based on dojo/Stateful and dojox/mvc/StatefulArray.
// This will be used to automatically keep various Todo properties (e.g. number of complete/incomplete items) consistent, and to auto-generate properties (e.g. default unique IDs).
modelClass: null,
getStatefulOptions: {
// summary:
// An object that specifies how plain object from Dojo Object Store should be converted to a data model based on dojo/Stateful and dojox/mvc/StatefulArray.
getType: function(/*Object*/ v){
return "specifiedModel"; // We are converting given object at once using modelClass here, instead of using per-data-item based data conversion callbacks
},
getStatefulSpecifiedModel: function(/*Object*/ o){
return new (this.parent.modelClass)(o); // Create an instance of modelClass given the plain object from Dojo Object Store
}
},
getPlainValueOptions: {
// summary:
// An object that specifies how a data model based on dojo/Stateful and dojox/mvc/StatefulArray should be converted to a plain object, to be saved to Dojo Object Store.
getType: function(/*todo/model/SimpleTodoModel*/ o){
return "specifiedModel"; // We are converting given data model at once, instead of using per-data-item based data conversion callbacks
},
getPlainSpecifiedModel: function(/*todo/model/SimpleTodoModel*/ o){
return { // Pick up only meaningful data here
id: o.id,
todos: array.map(o.todos, function(item){ return {title: item.title, completed: item.completed }; }),
incomplete: o.incomplete,
complete: o.complete
};
}
},
postscript: function(/*Object?*/ params, /*DOMNode?*/ srcNodeRef){
// summary:
// Kicks off instantiation of this controller, in a similar manner as dijit/_WidgetBase.postscript().
// params: Object?
// The optional parameters for this controller.
// srcNodeRef: DOMNode?
// The DOM node declaring this controller. Set if this controller is created via Dojo parser.
this.getStatefulOptions.parent = this; // For getStatefulOptions object to have a reference to this object
this.inherited(arguments);
srcNodeRef && srcNodeRef.setAttribute("widgetId", this.id); // If this is created via Dojo parser, set widgetId attribute so that destroyDescendants() of parent widget works
},
startup: function(){
// summary:
// A function called after the DOM fragment declaring this controller is added to the document, in a similar manner as dijit/_WidgetBase.startup().
this.inherited(arguments);
this.getStore(this.defaultId); // Obtain data from Dojo Object Store as soon as this starts up
},
set: function(/*String*/ name, /*Anything*/ value){
// summary:
// A function called when a property value is set to this controller.
if(name == this._refSourceModelProp && this[this._refSourceModelProp] != value && (this[this._refSourceModelProp] || {}).destroy){
this[this._refSourceModelProp].destroy(); // If we have a data model and it's being replaced, make sure it's cleaned up
}
this.inherited(arguments);
},
destroy: function(){
// summary:
// A function called when this controller is being destroyed, in a similar manner as dijit/_WidgetBase.destroy().
this.commit(); // Save the data to Dojo Object Store when this is destroyed
if((this[this._refSourceModelProp] || {}).destroy){
this[this._refSourceModelProp].destroy(); // If we have a data model, make sure it's cleaned up
}
this.inherited(arguments);
}
});
});
define(["dojo/_base/declare"], function(declare){
var ACTIVE = "/active",
COMPLETED = "/completed";
function getHiddenState(/*Object*/ props){
// summary:
// Returns the new hidden state of todo item, given the URL hash and the completed state of todo item.
// props: Object
// An object containing the URL hash and the completed state of todo item.
return props.hash == ACTIVE ? props.completed :
props.hash == COMPLETED ? !props.completed :
false;
}
return declare(null, {
// summary:
// A mix-in class for widgets-in-template for todo item, that looks at URL hash and completed state of todo item, and updates the hidden state.
// description:
// A todo item should be hidden if:
//
// - URL hash is "/active" and the todo item is complete -OR-
// - URL hash is "/copleted" and the todo item is incomplete
_setHashAttr: function(/*String*/ value){
// summary:
// Handler for calls to set("hash", val), to update hidden state given the new value and the completed state.
this.set("hidden", getHiddenState({hash: value, completed: this.completed})); // Update hidden state given the new value and the completed state
this._set("hash", value); // Assign the new value to the attribute
},
_setCompletedAttr: function(/*Boolean*/ value){
// summary:
// Handler for calls to set("completed", val), to update hidden state given the new value and the hash.
this.set("hidden", getHiddenState({hash: this.hash, completed: value})); // Update hidden state given the new value and the hash
this._set("completed", value); // Assign the new value to the attribute
}
});
});
// Used for avoiding dojo/cache from being loaded
define(function () {});
/**
* There's an incompatibility between the Dojo CheckBox and the Dojo MVC
* module. To use them together, I've manually tied the "checked" attribute
* value to push updates to the "value" attribute, which the Dojo MVC module
* expects.
*/
define(["dojo/_base/declare", "dijit/form/CheckBox"], function (declare, CheckBox) {
return declare("todo.form.CheckBox", [CheckBox], {
_setCheckedAttr: function (checked) {
this.inherited(arguments);
this._watchCallbacks("value", !checked, checked);
},
_getValueAttr: function () {
return this.get("checked");
}
});
});
/**
* Extension to the "InlineEditBox" widget to support editing on double click
* events rather than single clicks. We need to override base "postMixInProperties"
* lifecycle method where the event handlers are setup. This method is just a clone
* of that code with the modified event list.
*/
define([
"dijit/InlineEditBox",
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/dom-class"
], function (InlineEditBox, declare, lang, domClass) {
InlineEditBox._InlineEditor.prototype._onChange = function () {
if(this.inlineEditBox.autoSave && this.inlineEditBox.editing && this.enableSave()){
this._onBlur();
}
};
return declare("todo.form.InlineEditBox", InlineEditBox, {
postMixInProperties: function () {
// save pointer to original source node, since Widget nulls-out srcNodeRef
this.displayNode = this.srcNodeRef;
// connect handlers to the display node
var events = {
ondblclick: "_onClick",
onmouseover: "_onMouseOver",
onmouseout: "_onMouseOut",
onfocus: "_onMouseOver",
onblur: "_onMouseOut"
};
for(var name in events){
this.connect(this.displayNode, name, events[name]);
}
this.displayNode.setAttribute("role", "button");
if(!this.displayNode.getAttribute("tabIndex")){
this.displayNode.setAttribute("tabIndex", 0);
}
if(!this.value && !("value" in this.params)){ // "" is a good value if specified directly so check params){
this.value = lang.trim(this.renderAsHtml ? this.displayNode.innerHTML :
(this.displayNode.innerText||this.displayNode.textContent||""));
}
if(!this.value){
this.displayNode.innerHTML = this.noValueIndicator;
}
domClass.add(this.displayNode, 'dijitInlineEditBoxDisplayMode');
},
_onChange: function () {
this.inherited(arguments);
this._onBlur();
}
});
});
define({
// summary:
// A dojox/mvc data converter, that runs between todo/ctrl/HashController and a widget having <a> tag as its DOM node.
// It does one-way conversion from URL hash to boolean state of whether the URL hash matches href attribute of the widget's DOM node.
format: function(/*String*/ value){
// summary:
// Returns whether given value matches href attribute of the widget's DOM node.
return this.target.domNode.getAttribute("href").substr(1) == (value || "/");
},
parse: function(/*Boolean*/ value){
// summary:
// This functions throws an error so that the new value won't be reflected.
throw new Error();
}
});
define({
// summary:
// A dojox/mvc data converter, that does one-way conversion that returns whether we have less than n todo items in a specific state, where n is the given number in data converter options.
// Data converter options can be specified by setting constraints property in one of data binding endpoints.
// See data converter section of dojox/mvc/sync library's documentation for more details.
format: function(/*Number*/ value, /*Object*/ constraints){
// summary:
// Returns whether given value is less than or equal to the given number in data converter options (default zero).
return value <= (constraints.lessThanOrEqualTo || 0);
},
parse: function(/*Boolean*/ value){
// summary:
// This functions throws an error so that the new value won't be reflected.
throw new Error();
}
});
define([
"dojo/_base/array",
"dojo/_base/lang",
"dojox/mvc/getStateful"
], function(array, lang, getStateful){
var defaultData = {
// summary:
// Data used when there is no saved data.
// The same structure is used to save data to localStrage (when the page is unloaded).
// id: String
// The ID of saved data, used in Dojo Object Store as well as in localStorage that the Dojo Object Store uses.
id: "todos-dojo",
// todos: Object[]
// The todo items.
todos : [],
// incomplete: Number
// The count of incomplete todo items.
incomplete: 0,
// complete: Number
// The count of complete todo items.
complete: 0
};
var seq = 0; // The sequence number to generate unique IDs of todo items
return function(/*Object*/ data){
// summary:
// The data model for TodoMVC.
// description:
// This data model does:
//
// - Keep complete/incomplete properties up to date
// - Add unique ID to new todo items
//
// data: Object
// The plain data, coming from Dojo Object Store.
function assignPropertiesToNewItem(/*dojo/Stateful*/ item){
// summary:
// Add additional properties to a todo item.
// description:
// This function does:
//
// - Add unique ID to the given todo item
// - Add setter function for completed property to the given todo item
// item: dojo/Stateful
// The todo item.
lang.mixin(item, {
_completedSetter: function(/*Boolean*/ value){
// summary
// The setter function for completed property, which keeps complete/incomplete properties of the data model up to date.
// value: Boolean
// True means this todo item is completed. Otherwise means this todo item is incomplete.
if(this.completed ^ value){ // Update is done only if there is a flip in value
// If the todo item is turned to completed state, increase the count of completed items.
// If the todo item is turned to incomplete state, decrease the count of completed items.
statefulData.set("complete", statefulData.get("complete") + (value ? 1 : -1));
}
this.completed = value; // Assign the new value
},
// uniqueId: String
// The unique ID of the todo item.
uniqueId: "TODO-" + seq++
});
}
var statefulData = getStateful(data || defaultData); // Convert a plain object to dojo/Stateful, hierarchically. Use the default data if (lastly saved) data is not there
array.forEach(statefulData.todos, assignPropertiesToNewItem); // Add additional properties to todo items
return lang.mixin(statefulData, {
_elementWatchHandle: statefulData.todos.watchElements(function(/*Number*/ idx, /*dojo/Stateful[]*/ removals, /*dojo/Stateful[]*/adds){
// summary:
// The dojox/mvc/StatefulArray watch callback for adds/removals of todo items.
// description:
// This callback does:
//
// - Keep complete/incomplete properties up to date, upon adds/removals of todo items
// - Add unique ID to new todo items
// - Add setter function for completed property to new todo items
//
// idx: Number
// The array index where adds/removals happened at.
// removals: dojo/Stateful[]
// The removed todo items.
// adds: dojo/Stateful[]
// The added todo items.
var complete = statefulData.get("complete");
array.forEach(removals, function(item){ complete -= !!item.completed; }); // If completed items are removed, decrease the count of completed items
array.forEach(adds, function(item){ complete += !!item.completed; }); // If completed items are added, increase the count of completed items
statefulData.set("complete", complete);
statefulData.set("incomplete", statefulData.todos.get("length") - complete);
array.forEach(adds, assignPropertiesToNewItem); // Add additional properties to newly added todo items
}),
_incompleteSetter: function(/*Number*/ value){
// summary:
// dojo/Stateful setter function for incomplete property.
// description:
// This setter function keeps complete property up to date.
// value: Number
// The new count of incomplete todo items.
var changed = this.incomplete != value;
this.incomplete = value; // Assign the new value
if(changed){ // Update is done only if there is a change in value
this.set("complete", this.todos.get("length") - value); // Make the count of complete items reflect the new count of incomplete items
}
},
_completeSetter: function(/*Number*/ value){
// summary:
// dojo/Stateful setter function for complete property.
// description:
// This setter function keeps incomplete property up to date.
// value: Number
// The new count of complete todo items.
var changed = this.complete != value;
this.complete = value; // Assign the new value
if(changed){ // Update is done only if there is a change in value
this.set("incomplete", this.todos.get("length") - value); // Make the count of incomplete items reflect the new count of complete items
}
},
destroy: function(){
this._elementWatchHandle.remove(); // Stop watching for adds/removals of todo items
}
});
};
});
define(["dojo/_base/declare", "dojox/mvc/StatefulModel", "todo/store/LocalStorage", "dojox/mvc", "dojo/_base/lang", "dojo/_base/array"],
function(declare, StatefulModel, LocalStorage, mvc, lang, array) {
return declare([StatefulModel], {
/**
* Default model structure, overriden by any
* items found in localStorage.
*/
data: {
id: "todos-dojo",
todos : [],
incomplete: 0,
complete: 0
},
/**
* Initialise our custom dojo store, backed by localStorage. This will be
* used to read the initial items, if available, and persist the current items
* when the application finishes.
*/
store: new LocalStorage(),
constructor: function () {
/**
* Attempt to read todo items from localStorage,
* returning default value the first time the application
* is loaded... The "id" parameter is used as the unique
* localStorage key for this object.
*/
var data = this.store.get(this.data.id) || this.data;
this._createModel(data);
this.setUpModelBinding();
this.updateTotalItemsLeft();
},
setUpModelBinding: function () {
/**
* Set up a composite model attribute, "complete", that is automatically
* calculated whenever the "incomplete" source value is modified. The "complete"
* attribute is bound to a view widget, displaying the number of items that can
* be cleared using the link.
*/
mvc.bind(this.incomplete, "value", this.complete, "value", lang.hitch(this, function (value) {
return this.todos.get("length") - value;
}));
/**
* Bind all pre-populated todo items to update the
* total item values when the "completed" attribute is changed.
*/
array.forEach(this.todos, lang.hitch(this, "bindItemProps"));
/**
* Whenever the "todos" array is modified, an element is added
* or deleted, we need to recompute the model composite values.
* Binding attributes changes to the "onTodosModelChange" function
* using "watch", an attribute on dojo.Stateful objects.
*/
this.todos.watch(lang.hitch(this, "onTodosModelChange"));
},
/**
* Set up binding on a todo item, so that when the
* item's checked attribute changes, we re-calculate
* the composite model attribute's value, "complete".
*
* We also need to remove any tasks with empty titles.
*/
bindItemProps: function (item) {
mvc.bindInputs([item.completed], lang.hitch(this, "updateTotalItemsLeft"));
mvc.bindInputs([item.title], lang.hitch(window, setTimeout, lang.hitch(this, "deleteEmptyTasks")));
},
/**
* Search through current tasks list, removing all
* with empty titles.
*/
deleteEmptyTasks: function () {
var len = this.todos.length, idx = 0;
while (idx < len) {
if (!this.todos[idx].title.value.length) {
this.todos.remove(idx);
len--;
continue;
}
idx++;
}
},
/**
* When todos array is modified, we need to update the composite
* value attributes. If the modification was an addition, ensure the
* "completed" attribute is being watched for updates.
*/
onTodosModelChange: function (prop, oldValue, newValue) {
this.updateTotalItemsLeft();
if (typeof prop === "number" && !oldValue && newValue) {
this.bindItemProps(newValue);
}
},
/**
* Update the model's "incomplete" value with the
* total number of items not finished. This will automatically
* cause the bound "complete" value to be updated as well.
*/
updateTotalItemsLeft: function () {
this.incomplete.set("value", array.filter(this.todos, function (item) {
return item && !item.completed.value;
}).length);
}
});
});
/**
* Original source from https://gist.github.com/880822
* Converted to AMD-baseless format
*/
define(["dojo/_base/declare", "dojo/_base/lang", "dojo/_base/json", "dojo/store/util/QueryResults", "dojo/store/util/SimpleQueryEngine"],
function(declare, lang, json, QueryResults, SimpleQueryEngine) {
return declare(null, {
constructor: function(/*dojo.store.LocalStorage*/ options){
// summary:
// localStorage based object store.
// options:
// This provides any configuration information that will be mixed into the store.
// This should generally include the data property to provide the starting set of data.
if (!window.localStorage){
throw Error("LocalStorage not available on this device");
}
lang.mixin(this, options);
this.setData(this.data || []);
},
// idProperty: String
// Indicates the property to use as the identity property. The values of this
// property should be unique.
idProperty: "id",
// queryEngine: Function
// Defines the query engine to use for querying the data store
queryEngine: SimpleQueryEngine,
get: function(id){
// summary:
// Retrieves an object by its identity
// id: Number
// The identity to use to lookup the object
// returns: Object
// The object in the store that matches the given id.
return json.fromJson(localStorage.getItem(id));
},
getIdentity: function(object){
// summary:
// Returns an object's identity
// object: Object
// The object to get the identity from
// returns: Number
return object[this.idProperty];
},
put: function(object, options){
// summary:
// Stores an object
// object: Object
// The object to store.
// options: Object?
// Additional metadata for storing the data. Includes an "id"
// property if a specific id is to be used.
// returns: Number
var id = options && options.id || object[this.idProperty] || Math.random();
localStorage.setItem(id, json.toJson(object));
return id;
},
add: function(object, options){
// summary:
// Creates an object, throws an error if the object already exists
// object: Object
// The object to store.
// options: Object?
// Additional metadata for storing the data. Includes an "id"
// property if a specific id is to be used.
// returns: Number
if (this.get(object[this.idProperty])){
throw new Error("Object already exists");
}
return this.put(object, options);
},
remove: function(id){
// summary:
// Deletes an object by its identity
// id: Number
// The identity to use to delete the object
localStorage.removeItem(id);
},
query: function(query, options){
// summary:
// Queries the store for objects.
// query: Object
// The query to use for retrieving objects from the store.
// options: dojo.store.util.SimpleQueryEngine.__queryOptions?
// The optional arguments to apply to the resultset.
// returns: dojo.store.util.QueryResults
// The results of the query, extended with iterative methods.
//
// example:
// Given the following store:
//
// | var store = new dojo.store.LocalStorage({
// | data: [
// | {id: 1, name: "one", prime: false },
// | {id: 2, name: "two", even: true, prime: true},
// | {id: 3, name: "three", prime: true},
// | {id: 4, name: "four", even: true, prime: false},
// | {id: 5, name: "five", prime: true}
// | ]
// | });
//
// ...find all items where "prime" is true:
//
// | var results = store.query({ prime: true });
//
// ...or find all items where "even" is true:
//
// | var results = store.query({ even: true });
var data=[];
for (var i=0; i<localStorage.length;i++){
data.push(this.get(localStorage.key(i)));
}
return QueryResults(this.queryEngine(query, options)(data));
},
setData: function(data){
// summary:
// Sets the given data as the source for this store, and indexes it
// data: Object[]
// An array of objects to use as the source of data.
if(data.items){
// just for convenience with the data format IFRS expects
this.idProperty = data.identifier;
data = this.data = data.items;
}
for(var i = 0, l = data.length; i < l; i++){
var object = data[i];
this.put(object);
}
}
});
});
// TODO: Remove this file once Dojo provides this feature out-of-the-box.
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/has',
'dojo/json',
'dojo/store/util/QueryResults',
'dojo/store/util/SimpleQueryEngine'
], function (declare, lang, has, json, QueryResults, SimpleQueryEngine) {
'use strict';
has.add('array-findindex-api', typeof Array.prototype.findIndex === 'function');
function findIndex(a, callback, thisObject) {
if (has('array-findindex-api')) {
return a.findIndex(callback, thisObject);
} else {
for (var i = 0; i < a.length; i++) {
if (callback.call(thisObject, a[i])) {
return i;
}
}
return -1;
}
}
/**
* Dojo Object Store ({@link http://dojotoolkit.org/reference-guide/dojo/store.html})
* implementation of local storage for todo.
* @class TodoLocalStorage
*/
return declare(null, /** @lends TodoLocalStorage# */ {
/**
* The ID of local storage.
* @type {string}
*/
storageId: 'todos-dojo',
/**
* If the store has a single primary key, this indicates the property to use as the identity property.
* The values of this property should be unique.
*/
idProperty: 'id',
/**
* Defines the query engine to use for querying the data store.
* @type {Function}
*/
queryEngine: SimpleQueryEngine,
_loadStorage: function () {
return json.parse(localStorage.getItem(this.storageId) || '[]');
},
_saveStorage: function (data) {
localStorage.setItem(this.storageId, json.stringify(data));
},
/**
* dojo/_base/declare callback to mix parameter list into this instance.
* @param {Object} props The proerties to mix into this instance.
*/
postscript: function (props) {
lang.mixin(this, props);
},
/**
* Retrieves an object by its identity.
* @param {number} id The identity to use to lookup the object
* @returns {Object} The object in the store that matches the given id.
*/
get: function (id) {
var data = this._loadStorage();
var index = findIndex(data, function (item) {
return this.getIdentity(item) === id;
}, this);
if (index >= 0) {
return data[index];
}
},
/**
* @param {Object} object The object to get the identity from.
* @returns {number} The identify of the given object.
*/
getIdentity: function (object) {
return object[this.idProperty];
},
/**
* Stores an object.
* @param {Object} object The object to store.
* @param {Object} [options]
* Additional metadata for storing the data.
* Includes an `id` property if a specific id is to be used.
* @returns {number} The ID of the stored object.
*/
put: function (object, options) {
var id = options && options.id || this.getIdentity(object) || Math.random();
var data = this._loadStorage();
var index = findIndex(data, function (item) {
return this.getIdentity(item) === id;
}, this);
data[index >= 0 ? index : data.length] = object;
object[this.idProperty] = id;
this._saveStorage(data);
return id;
},
/**
* Creates an object, throws an error if the object already exists.
* @param {Object} object The object to store.
* @param {Object} [options]
* Additional metadata for storing the data.
* Includes an `id` property if a specific id is to be used.
*/
add: function (object, options) {
if (this.get(this.getIdentity(object))) {
throw new Error('Object already exists.');
}
return this.put(object, options);
},
/**
* Deletes an object by its identity.
* @param {number} id The identity to use to delete the object.
*/
remove: function (id) {
var data = this._loadStorage();
var index = findIndex(data, function (item) {
return this.getIdentity(item) === id;
}, this);
if (index < 0) {
throw new Error('Object not found.');
}
data.splice(index, 1);
this._saveStorage(data);
},
/**
* Queries the store for objects.
* @param {Object} query The query to use for retrieving objects from the store.
* @param {dojo/store/util/SimpleQueryEngine.__queryOptions} [options]
* The optional arguments to apply to the resultset.
* @returns {dojo/store/util/QueryResults} The results of the query, extended with iterative methods.
* @example
* <caption>
* Find all completed items.
* </caption>
* var results = store.query({completed: true});
*/
query: function (query, options) {
/*jshint newcap:false*/
return QueryResults(this.queryEngine(query, options)(this._loadStorage()));
}
});
});
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/when',
'dojo/Deferred',
'dojo/Stateful',
'dijit/registry',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'dojox/mvc/_InlineTemplateMixin',
'dojox/mvc/at',
'../computed',
'dojox/mvc/Element',
'./TodoEscape',
'./TodoFocus'
], function (declare,
lang,
when,
Deferred,
Stateful,
registry,
_WidgetBase,
_TemplatedMixin,
_WidgetsInTemplateMixin,
_InlineTemplateMixin,
at,
computed) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
/**
* Todo item, which does the following:
* - instantiate the template
* - expose the model for use in the template
* - provide event handlers
* @class Todo
*/
return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, _InlineTemplateMixin], {
startup: function () {
this.inherited(arguments);
if (!this.todosWidget) {
throw new Error('this.todosWidget property should be there before this widgets starts up: ' + this);
}
this.own(computed(this, 'isEditing', function (editedTodo) {
return editedTodo === this.target;
}, at(this.todosWidget, 'editedTodo')));
},
editTodo: function () {
this.set('originalTitle', this.target.get('title'));
this.todosWidget.set('editedTodo', this.target);
},
invokeSaveEdits: function () {
// For handling input's blur event, make sure change event has been fired
var dfd = new Deferred();
setTimeout(lang.hitch(this, function () {
when(this.saveEdits(), function (data) {
dfd.resolve(data);
}, function (e) {
dfd.reject(e);
});
}), 0);
return dfd.promise;
},
saveEdits: function (event) {
var progress;
var originalTitle = this.get('originalTitle');
var newTitle = lang.trim(this.target.get('title'));
var goAhead = this.get('isEditing') && newTitle !== originalTitle;
var blur = !event;
if (blur || goAhead) {
this.target.set('title', newTitle);
}
if (goAhead) {
if (newTitle) {
progress = this.todosWidget.saveTodo(this.target, originalTitle, this.target.get('completed'));
} else {
progress = this.removeTodo();
}
}
if (blur || goAhead) {
progress = when(progress, lang.hitch(this, function () {
this.todosWidget.set('editedTodo', null);
}), lang.hitch(this, function (e) {
this.todosWidget.set('editedTodo', null);
throw e;
}));
}
if (event && event.type === 'submit') {
event.preventDefault();
}
return progress;
},
revertEdits: function () {
if (this.get('isEditing')) {
this.todosWidget.set('editedTodo', null);
this.todosWidget.replaceTodo(this.target, new Stateful({
id: this.target.get('id'),
title: this.get('originalTitle'),
completed: this.target.get('completed')
}));
this.destroyRecursive();
}
},
toggleCompleted: function () {
this.todosWidget.saveTodo(this.target, this.target.get('title'), !this.target.get('completed'));
},
removeTodo: function () {
return when(this.todosWidget.removeTodo(this.target), lang.hitch(this, this.destroyRecursive));
}
});
});
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dijit/_WidgetBase'
], function (declare, lang, _WidgetBase) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
var ESCAPE_KEY = 27;
/**
* Widget that emits `escape` custom event when the element it is applied to gets an keydown event of escape key.
* @class TodoEscape
*/
return declare(_WidgetBase, {
postCreate: function () {
this.inherited(arguments);
this.on('keydown', lang.hitch(this, function (event) {
if (event.keyCode === ESCAPE_KEY) {
this.emit('escape');
}
}));
}
});
});
define([
'dojo/_base/declare',
'dijit/_WidgetBase',
'dijit/_FocusMixin' // For blur event support in data-dojo-attach-event
], function (declare, _WidgetBase, _FocusMixin) {
'use strict';
/**
* Widget that places focus on the element it is applied to when `shouldGetFocus` property becomes `true`.
* @class TodoFocus
*/
return declare([_WidgetBase, _FocusMixin], {
_setShouldGetFocusAttr: function (value) {
if (value) {
(this.focusNode && this.focusNode.nodeType === Node.ELEMENT_NODE ? this.focusNode : this.domNode).focus();
}
}
});
});
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/lang',
'dojo/router',
'dojo/when',
'dojo/Stateful',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'dojox/mvc/_InlineTemplateMixin',
'dojox/mvc/at',
'dojox/mvc/getStateful',
'dojox/mvc/StatefulArray',
'dojox/mvc/StoreRefController',
'../computed',
'../store/TodoLocalStorage',
'dojox/mvc/Element',
'dojox/mvc/WidgetList',
'./CSSToggleWidget'
], function (declare,
array,
lang,
router,
when,
Stateful,
_WidgetBase,
_TemplatedMixin,
_WidgetsInTemplateMixin,
_InlineTemplateMixin,
at,
getStateful,
StatefulArray,
StoreRefController,
computed,
TodoLocalStorage) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
/**
* Todo list, which does:
* - instantiate the template
* - retrieve and persist the model via the Dojo object store
* ({@link http://dojotoolkit.org/reference-guide/dojo/store.html})
* - expose the model for use in the template
* - provide event handlers
* @class Todos
*/
return declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, _InlineTemplateMixin, StoreRefController],
/** @lends Todos# */ {
store: null,
newTodo: '',
status: '',
saving: false,
filteredTodos: null,
remainingCount: 0,
completedCount: 0,
areAllChecked: false,
/**
* Specifies how a data model should be created from the retrieved data.
* @see http://dojotoolkit.org/reference-guide/dojox/mvc/getStateful.html
*/
getStatefulOptions: {
getType: function () {
return 'storeData';
},
getStatefulStoreData: function (data) {
return getStateful({todos: data});
}
},
emptyConverter: {
format: function (count) {
return count === 0;
},
parse: function () {
// No backward conversion
throw false;
}
},
pluralizeConverter: {
format: function (count) {
return count > 1 ? this.target.other : this.target.one;
},
parse: function () {
// No backward conversion
throw false;
}
},
statusConverter: {
format: function (status) {
return status === this.target.statusForElem;
},
parse: function () {
// No backward conversion
throw false;
}
},
postMixInProperties: function () {
if (!this.store) {
this.store = new TodoLocalStorage();
}
this.queryStore();
// Set up router
this.own(router.register(/.*/, lang.hitch(this, function (event) {
var table = {
'/active': 'active',
'/completed': 'completed'
};
this.set('status', table[event.newPath] || '');
})));
router.startup();
// Set up computed properties
var statusTable = {
active: false,
completed: true
};
this.own(computed(this, 'filteredTodos', lang.hitch(this, function (completed, status) {
var todos = this.get('todos');
if (!status) {
return todos;
} else {
var filteredTodos = [];
for (var i = 0; i < completed.length; i++) {
if (completed[i] === statusTable[status]) {
filteredTodos.push(todos[i]);
}
}
return filteredTodos;
}
}), lang.mixin(at(this.get('todos'), 'completed'), {each: true}), at(this, 'status')));
this.own(computed(this, 'remainingCount', function (completed) {
var count = 0;
for (var i = 0; i < completed.length; i++) {
count += +!completed[i];
}
return count;
}, lang.mixin(at(this.get('todos'), 'completed'), {each: true})));
this.own(computed(this, 'completedCount', function (completed) {
var count = 0;
for (var i = 0; i < completed.length; i++) {
count += +completed[i];
}
return count;
}, lang.mixin(at(this.get('todos'), 'completed'), {each: true})));
this.own(computed(this, 'areAllChecked', function (remainingCount) {
return remainingCount === 0;
}, at(this, 'remainingCount')));
this.inherited(arguments);
},
addTodo: function (event) {
var ret;
var title = lang.trim(this.get('newTodo'));
if (title.length > 0) {
this.set('saving', true);
var data = {
title: title,
completed: false
};
ret = when(this.addStore(data), lang.hitch(this, function () {
this.get('todos').push(new Stateful(data));
this.set('newTodo', '');
this.set('saving', false);
}), lang.hitch(this, function (e) {
this.set('saving', false);
throw e;
}));
}
event.preventDefault();
return ret;
},
saveTodo: function (todo, originalTitle, originalCompleted) {
this.set('saving', true);
var data = lang.mixin({}, todo);
delete data.isEditing;
return when(this.putStore(data), lang.hitch(this, function () {
this.set('saving', false);
}), lang.hitch(this, function (e) {
this.set('saving', false);
todo.set('title', originalTitle);
todo.set('completed', originalCompleted);
throw e;
}));
},
removeTodo: function (todo) {
this.set('saving', true);
return when(this.removeStore(this.store.getIdentity(todo)), lang.hitch(this, function () {
this.set('saving', false);
this.get('todos').splice(array.indexOf(this.get('todos'), todo), 1);
}), lang.hitch(this, function (e) {
this.set('saving', false);
throw e;
}));
},
replaceTodo: function (oldTodo, newTodo) {
var index = this.get('todos').indexOf(oldTodo);
if (index >= 0) {
this.get('todos').splice(index, 1, newTodo);
}
},
markAll: function () {
var current = this.get('areAllChecked');
array.forEach(this.get('todos'), function (todo) {
var old = todo.get('completed');
if (old !== current) {
todo.set('completed', current);
this.saveTodo(todo, todo.get('title'), old);
}
}, this);
},
clearCompletedTodos: function () {
array.forEach(this.get('todos').slice(), function (todo) {
if (todo.get('completed')) {
this.removeTodo(todo);
}
}, this);
}
});
});
{
"devDependencies": {
"intern": "^2.2.2"
},
"scripts": {
"test": "node node_modules/intern/bin/intern-runner.js config=test/intern reporters=runner reporters=lcovhtml"
}
}
// This is a build profile for Dojo version of TodoMVC.
dependencies = {
stripConsole: "normal",
// Dojo build profile for TodoMVC.
// Usage:
// > cd /path/to/todomvc/examples/dojo
// > bower install
// > ./bower_components/util/buildscripts/build.sh --profile ./profiles/todomvc.profile.js --release
selectorEngine:"acme",
/*jshint unused:false*/
var profile = {
// relative to this file
basePath: '../js',
layers: [
// relative to base path
releaseDir: './lib',
stripConsole: 'normal',
optimize: 'shrinksafe',
layerOptimize: 'shrinksafe',
packages: [
{
name: 'dojo',
location: '../bower_components/dojo'
},
{
name: 'dijit',
location: '../bower_components/dijit'
},
{
name: "dojo.js",
dependencies: [
"dojo.domReady",
"dojo.parser",
"todo.app18"
]
name: 'dojox',
location: '../bower_components/dojox'
}
],
prefixes: [
[ "dijit", "../dijit" ],
[ "dojox", "../dojox" ],
[ "doh", "../util/doh" ],
[ "todo", "../todomvc/architecture-examples/dojo/js/todo" ]
]
}
layers: {
'dojo/dojo': {
include: [
// dojo, dijit and dojox modules from js/todo/*.js
'dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/lang',
'dojo/router',
'dojo/when',
'dojo/Stateful',
'dojo/has',
'dojo/json',
'dojo/store/util/QueryResults',
'dojo/store/util/SimpleQueryEngine',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'dijit/_FocusMixin',
'dojox/mvc/_InlineTemplateMixin',
'dojox/mvc/at',
'dojox/mvc/getStateful',
'dojox/mvc/StatefulArray',
'dojox/mvc/StoreRefController',
'dojox/mvc/Element',
'dojox/mvc/WidgetList'
],
boot: true,
customBase: true
}
},
staticHasFeatures: {
'dojo-sync-loader': false,
},
selectorEngine: 'lite',
defaultConfig: {
hasCache: {
'config-selectorEngine': 'lite'
},
async: true
}
};
......@@ -12,17 +12,13 @@ The [Dojo website](http://dojotoolkit.org) is a great resource for getting start
Here are some links you may find helpful:
* [Documentation](http://dojotoolkit.org/documentation)
* [Getting started guide](https://dojotoolkit.org/reference-guide/1.8/quickstart)
* [Getting started guide](https://dojotoolkit.org/reference-guide/quickstart)
* [API Reference](http://dojotoolkit.org/api)
* [Blog](http://dojotoolkit.org/blog)
Articles and guides from the community:
* [Getting StartED with Dojo](http://startdojo.com)
Get help from other Dojo users:
* [Dojo on StackOverflow](http://stackoverflow.com/questions/tagged/dojo)
* [Dojo/MVC on StackOverflow](http://stackoverflow.com/questions/tagged/dojo+model-view-controller)
* [Mailing list](http://dojotoolkit.org/community)
* [Dojo on Twitter](http://twitter.com/dojo)
......@@ -31,30 +27,15 @@ _If you have other helpful links to share, or find any of the links above no lon
## Running
To build the Dojo app, you first need to download and extract the [Dojo SDK](https://dojotoolkit.org/download/#sdk).
At the root folder of the extracted SDK, copy or symlink the complete `todomvc`
folder, so your directory structure looks like this:
dojo-release/
├── dijit
├── dojo
├── dojox
├── todomvc
└── util
Enter the `dojo-release/util` folder and run these commands to build the `dojo.js` file including all required resources and copy it back into the todomvc folder. You need either java or node on your system to run these:
To run the Dojo example, you need to build Dojo with TodoMVC profile.
To do so, do the following. You need either java or node on your system to run these:
```
buildscripts/build.sh --profile ../todomvc/architecture-examples/dojo/profiles/todomvc.profile.js -r
cp ../release/dojo/dojo/dojo.js ../todomvc/architecture-examples/dojo/js/lib/dojo-1.8/dojo.js
cd /path/to/todomvc/examples/dojo
bower install
./bower_components/util/buildscripts/build.sh --profile ./profiles/todomvc.profile.js --release
```
After a new release of Dojo, you may need to copy more files for this to work.
Check out the `js/lib/` folder for other files that are required from the
build.
You can now open the app in your browser.
## AMD Notes
......
define([
'dojo/_base/declare',
'dojo/router/RouterBase'
], function (declare, RouterBase) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
return new (declare(RouterBase, {
_handlePathChange: function () {
this.inherited(arguments);
return false; // Prevent actual hash change
}
}))({});
});
define((function () {
'use strict';
var list = ['./allNode'];
if (typeof window !== 'undefined') {
list.push('./allBrowser');
}
return list;
})(), 1);
define([
'./todo/store/TodoLocalStorage',
'./todo/widgets/CSSToggleWidget',
'./todo/widgets/Todo',
'./todo/widgets/TodoEscape',
'./todo/widgets/TodoFocus',
'./todo/widgets/Todos'
], 1);
define([
'./todo/computed'
], 1);
define(function () {
'use strict';
/**
* @param {Object[]} handles The handles that should be cleaned up after each tests.
* @returns {Function} The function that should be called after each tests to clean up the handles.
*/
return function (handles) {
return function () {
for (var handle = null; (handle = handles.shift());) {
if (typeof handle.close === 'function') {
handle.close();
} else if (typeof handle.remove === 'function') {
handle.remove();
} else if (typeof handle.destroyRecursive === 'function') {
handle.destroyRecursive();
} else if (typeof handle.destroy === 'function') {
handle.destroy();
} else {
throw new Error('Handle cannot be cleaned up.');
}
}
};
};
});
// To run the test cases:
// With node.js:
// > cd /path/to/todomvc/examples/dojo/
// > node node_modules/intern/bin/intern-client.js config=test/intern
// With node.js and WebDriver:
// > cd /path/to/todomvc/examples/dojo/
// > npm test
// With browser:
// > cd /path/to/todomvc
// > gulp serve
// Then hit: http://localhost:8080/examples/dojo/node_modules/intern/client.html?config=test/intern
define((function (global) {
'use strict';
// dojoConfig needs to be defined here, otherwise it's too late to affect the dojo loader api
global.dojoConfig = {
async: true
};
return {
loader: {
baseUrl: typeof window !== 'undefined' ? '../..' : '.',
packages: [
{
name: 'dojo',
location: './bower_components/dojo'
},
{
name: 'dijit',
location: './bower_components/dijit'
},
{
name: 'dojox',
location: './bower_components/dojox'
},
{
name: 'todo',
location: './js/todo'
},
{
name: 'test',
location: './test'
}
],
map: {
'todo/widgets/Todos': {
'dojo/router': 'test/RouterMock'
},
'test/todo/widgets/Todos': {
'dojo/router': 'test/RouterMock'
}
}
},
useLoader: {
'host-node': 'dojo/dojo',
'host-browser': '../../bower_components/dojo/dojo.js'
},
proxyPort: 9000,
proxyUrl: 'http://localhost:9000/',
capabilities: {
'selenium-version': '2.44.0',
'idle-timeout': 60
},
environments: [
{browserName: 'internet explorer'},
{browserName: 'firefox'},
{browserName: 'chrome'},
{browserName: 'safari'}
],
maxConcurrency: 3,
useSauceConnect: false,
webdriver: {
host: 'localhost',
port: 4444
},
suites: ['test/all'],
excludeInstrumentation: /^(bower_components|node_modules|test)/
};
})(this));
define([
'intern!bdd',
'intern/chai!expect',
'dojo/_base/lang',
'dojox/mvc/at',
'dojox/mvc/getStateful',
'todo/computed',
'../handleCleaner'
], function (bdd, expect, lang, at, getStateful, computed, handleCleaner) {
'use strict';
// For supporting Intern's true/false check
/*jshint -W030*/
bdd.describe('Test todo/computed', function () {
var handles = [];
bdd.afterEach(handleCleaner(handles));
bdd.it('Check parameters', function () {
var throwCount = 0;
try {
computed(undefined, 'foo');
} catch (e) {
++throwCount;
}
try {
computed(null, 'foo');
} catch (e) {
++throwCount;
}
try {
computed({}, '*');
} catch (e) {
++throwCount;
}
try {
computed({}, 'foo', function () {}, at({}, '*'));
} catch (e) {
++throwCount;
}
expect(throwCount).to.equal(4);
});
bdd.it('Computed property - Observable', function () {
var count = 0;
var stateful = getStateful({
first: 'John',
last: 'Doe'
});
handles.push(computed(stateful, 'name', function (first, last) {
return first + ' ' + last;
}, at(stateful, 'first'), at(stateful, 'last')));
handles.push(computed(stateful, 'nameLength', function (name) {
return name.length;
}, at(stateful, 'name')));
handles.push(stateful.watch('name', function (name, old, current) {
expect(current).to.equal('Ben Doe');
expect(old).to.equal('John Doe');
count++;
}));
handles.push(stateful.watch('nameLength', function (name, old, current) {
expect(current).to.equal(7);
expect(old).to.equal(8);
count++;
}));
expect(stateful.get('name')).to.equal('John Doe');
expect(stateful.get('nameLength')).to.equal(8);
stateful.set('first', 'Ben');
expect(count).to.equal(2);
});
bdd.it('Computed property - Non-observable', function () {
var o = {
first: 'John',
last: 'Doe'
};
handles.push(computed(o, 'name', function (first, last) {
return first + ' ' + last;
}, at(o, 'first'), at(o, 'last')));
handles.push(computed(o, 'nameLength', function (name) {
return name.length;
}, at(o, 'name')));
expect(o.name).to.equal('John Doe');
expect(o.nameLength).to.equal(8);
});
bdd.it('Computed array - Observable', function () {
var count = 0;
var stateful = getStateful({
items: [
{name: 'Anne Ackerman'},
{name: 'Ben Beckham'},
{name: 'Chad Chapman'},
{name: 'Irene Ira'}
],
countShortLessThanFour: true
});
var callbacks = [
function (length, oldLength) {
expect(length).to.equal(57);
expect(oldLength).to.equal(45);
stateful.items[4].set('name', 'John Doe');
},
function (length, oldLength) {
expect(length).to.equal(53);
expect(oldLength).to.equal(57);
stateful.set('countShortLessThanFour', false);
},
function (length, oldLength) {
expect(length).to.equal(42);
expect(oldLength).to.equal(53);
}
];
handles.push(computed(stateful, 'totalNameLength', function (a, countShortLessThanFour) {
var total = 0;
for (var i = 0; i < a.length; i++) {
var first = a[i].split(' ')[0];
total += (countShortLessThanFour || first.length >= 4 ? a[i].length : 0);
}
return total;
}, lang.mixin(at(stateful.items, 'name'), {each: true}), at(stateful, 'countShortLessThanFour')));
handles.push(stateful.watch('totalNameLength', function (name, old, current) {
callbacks[count++](current, old);
}));
expect(stateful.get('totalNameLength')).to.equal(45);
stateful.items.push(getStateful({name: 'John Jacklin'}));
expect(count).to.equal(3);
});
bdd.it('Computed array - Non-observable', function () {
var o = {
items: [
{name: 'Anne Ackerman'},
{name: 'Ben Beckham'},
{name: 'Chad Chapman'},
{name: 'Irene Ira'}
],
countShortLessThanFour: true
};
handles.push(computed(o, 'totalNameLength', function (a, countShortLessThanFour) {
var total = 0;
for (var i = 0; i < a.length; i++) {
var first = a[i].split(' ')[0];
total += (countShortLessThanFour || first.length >= 4 ? a[i].length : 0);
}
return total;
}, lang.mixin(at(o.items, 'name'), {each: true}), at(o, 'countShortLessThanFour')));
expect(o.totalNameLength).to.equal(45);
});
bdd.it('Computed property in array', function () {
var called;
var statefulArray = getStateful([
'foo'
]);
handles.push(computed(statefulArray, 1, function (foo) {
return '*' + foo + '*';
}, at(statefulArray, 0)));
handles.push(statefulArray.watch(1, function (name, old, current) {
expect(current).to.equal('*bar*');
expect(old).to.equal('*foo*');
called = true;
}));
expect(statefulArray[1]).to.equal('*foo*');
statefulArray.set(0, 'bar');
expect(called).to.be.true;
});
bdd.it('Error in computed property callback', function () {
var count = 0;
var stateful = getStateful({
first: 'John',
last: 'Doe'
});
handles.push(computed(stateful, 'name', function (first, last) {
if (first === 'John') {
throw undefined;
}
return first + ' ' + last;
}, at(stateful, 'first'), at(stateful, 'last')));
handles.push(computed(stateful, 'nameLength', function (name) {
return name.length;
}, at(stateful, 'name')));
handles.push(stateful.watch('name', function (name, old, current) {
expect(current).to.equal('Ben Doe');
expect(old).to.be.undefined;
count++;
}));
handles.push(stateful.watch('nameLength', function (name, old, current) {
expect(current).to.equal(7);
expect(old).to.be.undefined;
count++;
}));
expect(stateful.get('name')).to.be.undefined;
expect(stateful.get('nameLength')).to.be.undefined;
stateful.set('first', 'Ben');
expect(count).to.equal(2);
});
bdd.it('Checking for same value', function () {
var dfd = this.async(1000);
var shouldChange = true;
var count = 0;
var stateful = getStateful({
foo: NaN
});
handles.push(computed(stateful, 'computed', dfd.rejectOnError(function (foo) {
if (!shouldChange) {
throw new Error('Change is detected even though there is no actual change.');
}
++count;
return foo;
}), at(stateful, 'foo')));
shouldChange = false;
stateful.set('foo', NaN);
shouldChange = true;
stateful.set('foo', -0);
stateful.set('foo', +0);
expect(count).to.equal(3);
dfd.resolve(1);
});
bdd.it('Cleaning up computed properties/arrays', function () {
var stateful = getStateful({
first: 'John',
last: 'Doe',
items: [
{name: 'Anne Ackerman'},
{name: 'Ben Beckham'},
{name: 'Chad Chapman'},
{name: 'Irene Ira'}
]
});
var computeHandleName = computed(stateful, 'name', function (first, last) {
return first + ' ' + last;
}, at(stateful, 'first'), at(stateful, 'last'));
handles.push(computeHandleName);
var computeHandleTotalNameLength = computed(stateful, 'totalNameLength', function (names) {
var total = 0;
for (var i = 0; i < names.length; i++) {
total += names[i].length;
}
return total;
}, lang.mixin(at(stateful.items, 'name'), {each: true}));
handles.push(computeHandleTotalNameLength);
handles.push(stateful.watch('nameLength', function () {
throw new Error('Watch callback shouldn\'t be called for removed computed property.');
}));
handles.push(stateful.watch('totalNameLength', function () {
throw new Error('Watch callback shouldn\'t be called for removed computed array.');
}));
computeHandleName.remove();
computeHandleTotalNameLength.remove();
stateful.set('first', 'Ben');
stateful.items.push({name: 'John Jacklin'});
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/json',
'dojo/when',
'todo/store/TodoLocalStorage',
'../../handleCleaner'
], function (bdd, expect, json, when, TodoLocalStorage, handleCleaner) {
'use strict';
var STORAGE_ID = 'todos-dojo-test';
// For supporting Intern's true/false check
/*jshint -W030*/
bdd.describe('Test todo/TodoLocalStorage', function () {
var handles = [];
bdd.beforeEach(function () {
localStorage.setItem(STORAGE_ID, json.stringify([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
}
]));
});
bdd.afterEach(handleCleaner(handles));
bdd.it('Query', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.query({completed: true}), dfd.callback(function (data) {
expect(data.slice()).to.deep.equal([
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
}
]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Query - Empty storage', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
localStorage.removeItem(STORAGE_ID);
when(store.query(), dfd.callback(function (data) {
expect(data.slice()).to.deep.equal([]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Get', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.get(2), dfd.callback(function (data) {
expect(data).to.deep.equal({
title: 'Bar',
completed: true,
id: 2
});
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Add - New', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.add({
title: 'Qux',
completed: false
}), dfd.callback(function (id) {
expect(json.parse(localStorage.getItem(STORAGE_ID))).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
},
{
title: 'Qux',
completed: false,
id: id
}
]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Add - Existing', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
try {
when(store.add({
title: 'Baz',
completed: true,
id: 3
}), function () {
dfd.reject(new Error('add() with existing ID shouldn\'t succeed.'));
}, function () {
dfd.resolve(1);
});
} catch (e) {
dfd.resolve(1);
}
});
bdd.it('Put - New', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.put({
title: 'Qux',
completed: false
}), dfd.callback(function (id) {
expect(json.parse(localStorage.getItem(STORAGE_ID))).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
},
{
title: 'Qux',
completed: false,
id: id
}
]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Put - Existing', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.put({
title: 'Baz',
completed: false
}, {id: 3}), dfd.callback(function () {
expect(json.parse(localStorage.getItem(STORAGE_ID))).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: false,
id: 3
}
]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Remove - Existing', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
when(store.remove(3), dfd.callback(function () {
expect(json.parse(localStorage.getItem(STORAGE_ID))).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
}
]);
}), function (e) {
dfd.reject(e);
});
});
bdd.it('Remove - Non-existing', function () {
var dfd = this.async(1000);
var store = new TodoLocalStorage({storageId: STORAGE_ID});
try {
when(store.remove(-1), function () {
dfd.reject(new Error('remove() with non-existent ID should not succeed.'));
}, function () {
dfd.resolve(1);
});
} catch (e) {
dfd.resolve(1);
}
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/_base/declare',
'dojo/dom-class',
'dijit/_WidgetBase',
'dijit/_TemplatedMixin',
'dijit/_WidgetsInTemplateMixin',
'todo/widgets/CSSToggleWidget',
'../../handleCleaner',
'dojo/text!./templates/CSSToggleWidget.html',
'dojox/mvc/at'
], function (bdd, expect, declare, domClass,
_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin, CSSToggleWidget, handleCleaner, template) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
// For supporting Intern's true/false check
/*jshint -W030*/
bdd.describe('Test todo/widgets/CSSToggleWidget', function () {
var handles = [];
bdd.afterEach(handleCleaner(handles));
bdd.it('Standalone', function () {
var w = new (declare(CSSToggleWidget, {
buildRendering: function () {
this.inherited(arguments);
this.fooNode = this.domNode.appendChild(document.createElement('div'));
this.barNode = this.domNode.appendChild(document.createElement('div'));
},
_setDijitDisplayNoneAttr: [
{
node: 'fooNode',
type: 'classExists'
},
{
node: 'barNode',
type: 'classExists',
className: 'dijitHidden'
}
]
}))({
dijitDisplayNone: true
});
handles.push(w);
expect(domClass.contains(w.fooNode, 'dijitDisplayNone')).to.be.true;
expect(domClass.contains(w.barNode, 'dijitHidden')).to.be.true;
w.set('dijitDisplayNone', false);
expect(domClass.contains(w.fooNode, 'dijitDisplayNone')).to.be.false;
expect(domClass.contains(w.barNode, 'dijitHidden')).to.be.false;
});
bdd.it('In template', function () {
var w = new (declare([_WidgetBase, _TemplatedMixin, _WidgetsInTemplateMixin], {
templateString: template
}))({
teeny: true
});
handles.push(w);
w.startup();
expect(domClass.contains(w.testNode.domNode, 'dijitTeeny')).to.be.true;
w.set('teeny', false);
expect(domClass.contains(w.testNode.domNode, 'dijitTeeny')).to.be.false;
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/_base/lang',
'dojo/when',
'dojo/Deferred',
'dojo/Stateful',
'todo/widgets/Todo',
'../../handleCleaner'
], function (bdd, expect, lang, when, Deferred, Stateful, Todo, handleCleaner) {
'use strict';
// For supporting Intern's true/false check
/*jshint -W030*/
bdd.describe('Test todo/widgets/Todo', function () {
var w;
var handles = [];
var emptyTemplateString = '<div><\/div>';
bdd.beforeEach(function () {
// Create fresh Todo instance for each test case
handles.push(w = new Todo({
templateString: emptyTemplateString,
target: new Stateful({
id: 1,
title: 'Foo',
completed: false
}),
todosWidget: new Stateful({
saveTodo: function (todo, originalTitle, originalCompleted) {
w.saveTodoArgs = {
todo: todo,
originalTitle: originalTitle,
originalCompleted: originalCompleted
};
},
replaceTodo: function (oldTodo, newTodo) {
w.replaceTodoArgs = {
oldTodo: oldTodo,
newTodo: newTodo
};
},
removeTodo: function (todo) {
w.removeTodoArgs = {todo: todo};
}
})
}));
w.startup();
});
bdd.afterEach(handleCleaner(handles));
bdd.it('Missing todosWidget reference', function () {
var caught;
var w = new Todo({
templateString: emptyTemplateString
});
handles.push(w);
try {
w.startup();
} catch (e) {
caught = true;
}
expect(caught).to.be.true;
});
bdd.it('Start editing title', function () {
expect(w.get('isEditing')).not.to.be.true;
w.editTodo();
expect(w.get('isEditing')).to.be.true;
w.target.set('title', 'Bar');
expect(w.get('originalTitle')).to.equal('Foo');
});
bdd.it('Save editing title', function () {
w.saveEdits();
expect(w.saveTodoArgs).not.to.exist;
var preventedDefault;
var fakeSubmitEvent = {
type: 'submit',
preventDefault: function () {
preventedDefault = true;
}
};
w.editTodo();
w.target.set('title', 'Bar');
w.saveEdits(fakeSubmitEvent);
expect(preventedDefault).to.be.true;
expect(w.saveTodoArgs.todo.get('title')).to.equal('Bar');
expect(w.saveTodoArgs.originalTitle).to.equal('Foo');
expect(w.get('isEditing')).not.to.be.true;
preventedDefault = false;
w.saveTodoArgs = null;
w.editTodo();
w.target.set('title', ' Bar ');
w.saveEdits(fakeSubmitEvent);
expect(w.saveTodoArgs).not.to.be.ok;
expect(w.target.get('title')).to.equal(' Bar ');
expect(w.get('isEditing')).to.be.true;
expect(preventedDefault).to.be.true;
w.editTodo();
w.target.set('title', 'Bar0');
return w.invokeSaveEdits().then(function () {
expect(w.target.get('title')).to.equal('Bar0');
expect(w.get('isEditing')).not.to.be.true;
}).then(function () {
w.todosWidget.saveTodo = function (todo, originalTitle, originalCompleted) {
todo.set('title', originalTitle);
todo.set('completed', originalCompleted);
var dfd = new Deferred();
dfd.reject(new Error());
return dfd.promise;
};
w.editTodo();
w.target.set('title', 'Bar1');
return w.invokeSaveEdits().then(function () {
throw new Error('invokeSaveEdits() shouldn\'t succeed if saveTodo() doesn\'t.');
}, function () {
expect(w.target.get('title')).to.equal('Bar0');
expect(w.get('isEditing')).not.to.be.true;
});
});
});
bdd.it('Cancel editing title', function () {
w.revertEdits();
expect(w.replaceTodoArgs).not.to.exist;
w.editTodo();
w.target.set('title', 'Bar');
w.revertEdits();
expect(w.replaceTodoArgs.oldTodo.get('title')).to.equal('Bar');
expect(w.replaceTodoArgs.newTodo.get('title')).to.equal('Foo');
});
bdd.it('Toggling completed state', function () {
w.target.set('completed', true);
w.toggleCompleted();
expect(w.saveTodoArgs.todo.get('completed')).to.be.true;
expect(w.saveTodoArgs.originalCompleted).not.to.be.true;
w.target.set('completed', false);
w.toggleCompleted();
expect(w.saveTodoArgs.todo.get('completed')).not.to.be.true;
expect(w.saveTodoArgs.originalCompleted).to.be.true;
});
bdd.it('Removing todo', function () {
w.removeTodo();
expect(w.removeTodoArgs.todo).to.deep.equal(w.target);
expect(w._destroyed).to.be.true;
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/on',
'todo/widgets/TodoEscape',
'../../handleCleaner'
], function (bdd, expect, on, TodoEscape, handleCleaner) {
'use strict';
var ESCAPE_KEY = 27;
bdd.describe('Test todo/widgets/TodoEscape', function () {
var handles = [];
bdd.afterEach(handleCleaner(handles));
bdd.it('Emitting custom event', function () {
var count = 0;
var w = new TodoEscape().placeAt(document.body);
handles.push(w);
w.on('escape', function () {
++count;
});
w.startup();
w.emit('keydown', {
keyCode: ESCAPE_KEY
});
w.emit('keydown', {});
expect(count).to.equal(1);
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/_base/declare',
'dijit/_TemplatedMixin',
'todo/widgets/TodoFocus',
'../../handleCleaner',
'dojo/text!./templates/TodoFocus.html'
], function (bdd, expect, declare, _TemplatedMixin, TodoFocus, handleCleaner, template) {
'use strict';
bdd.describe('Test todo/widgets/TodoFocus', function () {
var handles = [];
bdd.afterEach(handleCleaner(handles));
bdd.it('Focus on domNode', function () {
var w = new TodoFocus({}, document.createElement('input')).placeAt(document.body);
handles.push(w);
w.set('shouldGetFocus', false);
expect(document.activeElement).not.to.equal(w.domNode);
w.set('shouldGetFocus', true);
expect(document.activeElement).to.equal(w.domNode);
});
bdd.it('Focus on focusNode defined in template', function () {
var w = new (declare([TodoFocus, _TemplatedMixin], {
templateString: template
}))().placeAt(document.body);
handles.push(w);
w.set('shouldGetFocus', true);
expect(document.activeElement).to.equal(w.focusNode);
});
});
});
define([
'intern!bdd',
'intern/chai!expect',
'dojo/_base/array',
'dojo/_base/declare',
'dojo/router',
'dojo/Deferred',
'dojo/Stateful',
'dojo/store/Memory',
'dojox/mvc/at',
'dojox/mvc/getPlainValue',
'todo/widgets/Todos',
'../../handleCleaner'
], function (bdd, expect, array, declare, router, Deferred, Stateful, Memory, at, getPlainValue, Todos, handleCleaner) {
// To use Dojo's super call method, inherited()
/*jshint strict:false*/
// For supporting Intern's true/false check
/*jshint -W030*/
bdd.describe('Test todo/widgets/Todos', function () {
var w;
var handles = [];
var initialData = [
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
}
];
bdd.afterEach(handleCleaner(handles));
var emptyTemplateString = '<div><\/div>';
var TestTodos = declare(Todos, {
templateString: emptyTemplateString,
one: 'item left',
other: 'items left',
statusForElem: 'active',
postMixInProperties: function () {
this.inherited(arguments);
this.set('isEmpty', at(this.get('todos'), 'length').transform(this.emptyConverter));
this.set('remainingCountMessage', at(this, 'remainingCount').transform(this.pluralizeConverter));
this.set('statusMatches', at(this, 'status').transform(this.statusConverter));
}
});
bdd.beforeEach(function () {
// Create fresh Todo instance for each test case
handles.push(w = new TestTodos({
store: new Memory({data: initialData.slice()})
}));
w.startup();
});
bdd.it('Default store', function () {
var w = new Todos({
templateString: emptyTemplateString
});
handles.push(w);
expect(w.store.storageId).to.equal('todos-dojo');
});
bdd.it('Initial data', function () {
expect(getPlainValue(w.get('todos'))).to.deep.equal(initialData);
expect(w.get('remainingCount')).to.equal(1);
expect(w.get('completedCount')).to.equal(2);
expect(w.get('areAllChecked')).not.to.be.true;
expect(w.get('isEmpty')).not.to.be.true;
expect(w.get('remainingCountMessage')).to.equal('item left');
expect(w.get('statusMatches')).not.to.be.true;
});
bdd.it('Routing', function () {
expect(getPlainValue(w.get('filteredTodos'))).to.deep.equal(initialData);
router.go('/active');
expect(getPlainValue(w.get('filteredTodos'))).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
}
]);
expect(w.get('statusMatches')).to.be.true;
router.go('/completed');
expect(getPlainValue(w.get('filteredTodos'))).to.deep.equal([
{
title: 'Bar',
completed: true,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
}
]);
expect(w.get('statusMatches')).not.to.be.true;
router.go('/');
expect(getPlainValue(w.get('filteredTodos'))).to.deep.equal(initialData);
expect(w.get('statusMatches')).not.to.be.true;
router.go('');
expect(getPlainValue(w.get('filteredTodos'))).to.deep.equal(initialData);
expect(w.get('statusMatches')).not.to.be.true;
});
bdd.it('Adding todo', function () {
var preventedDefault;
var fakeEvent = {
preventDefault: function () {
preventedDefault = true;
}
};
w.set('newTodo', ' ');
w.addTodo(fakeEvent);
expect(preventedDefault).to.be.true;
expect(w.store.data).to.deep.equal(initialData);
preventedDefault = false;
w.set('newTodo', 'Qux');
w.addTodo(fakeEvent);
expect(preventedDefault).to.be.true;
var newOne = array.filter(w.store.data, function (entry) {
return [1, 2, 3].indexOf(entry.id) < 0;
});
expect(newOne.length).to.equal(1);
expect(newOne[0].title).to.equal('Qux');
expect(newOne[0].completed).not.to.be.true;
expect(w.get('remainingCountMessage')).to.equal('items left');
});
bdd.it('Saving change', function () {
w.saveTodo({
title: 'Bar0',
completed: false,
id: 2
});
expect(w.store.data).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Bar0',
completed: false,
id: 2
},
{
title: 'Baz',
completed: true,
id: 3
}
]);
});
bdd.it('Removing todo', function () {
w.removeTodo(initialData[1]);
expect(w.store.data).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
},
{
title: 'Baz',
completed: true,
id: 3
}
]);
w.removeTodo(initialData[0]);
w.removeTodo(initialData[2]);
expect(w.get('isEmpty')).to.be.true;
});
bdd.it('Reverting todo', function () {
var todo = w.get('todos')[1];
var original = {
title: todo.get('title'),
completed: todo.get('completed'),
id: todo.get('id')
};
todo.set('title', 'Bar0');
w.replaceTodo(todo, original);
expect(getPlainValue(w.get('todos'))).to.deep.equal(initialData);
w.replaceTodo({}, original);
expect(getPlainValue(w.get('todos'))).to.deep.equal(initialData);
});
bdd.it('Marking all todos as complete', function () {
w.set('areAllChecked', !w.get('areAllChecked'));
w.markAll();
expect(array.every(w.store.data, function (todo) {
return todo.completed;
})).to.be.true;
w.set('areAllChecked', !w.get('areAllChecked'));
w.markAll();
expect(array.every(w.store.data, function (todo) {
return !todo.completed;
})).to.be.true;
});
bdd.it('Removing completed todos', function () {
w.clearCompletedTodos();
expect(w.store.data).to.deep.equal([
{
title: 'Foo',
completed: false,
id: 1
}
]);
});
bdd.it('Error accessing store', function () {
function failPromiseMethod() {
var dfd = new Deferred();
dfd.reject(new Error());
return dfd.promise;
}
var w = new Todos({
templateString: emptyTemplateString,
store: {
add: failPromiseMethod,
put: failPromiseMethod,
remove: failPromiseMethod,
query: function () {
return [];
},
getIdentity: function () {}
}
});
handles.push(w);
w.set('newTodo', 'Foo');
return w.addTodo(new CustomEvent('dummy')).then(function () {
throw new Error('addTodo() shouldn\'t succeed if the corresponding store method doesn\'t.');
}, function () {
expect(w.get('saving')).not.to.be.true;
}).then(function () {
var todo = new Stateful({
title: 'Foo1',
completed: true,
id: 1
});
return w.saveTodo(todo, 'Foo0', false).then(function () {
throw new Error('saveTodo() shouldn\'t succeed if the corresponding store method doesn\'t.');
}, function () {
expect(w.get('saving')).not.to.be.true;
expect(todo.get('title')).to.equal('Foo0');
expect(todo.get('completed')).not.to.be.true;
});
}).then(function () {
return w.removeTodo().then(function () {
throw new Error('removeTodo() shouldn\'t succeed if the corresponding store method doesn\'t.');
}, function () {
expect(w.get('saving')).not.to.be.true;
});
});
});
});
});
<div>
<script type="dojo/require">
at: 'dojox/mvc/at'
</script>
<div data-dojo-attach-point="testNode" data-dojo-type="todo/widgets/CSSToggleWidget" data-dojo-props="_setTeenyAttr: {type: 'classExists', className: 'dijitTeeny'}, teeny: at(this, 'teeny')"></div>
</div>
<div>
<a data-dojo-attach-point="focusNode" href="">Foo</a>
</div>
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