Commit a8ebd557 authored by Sindre Sorhus's avatar Sindre Sorhus

Merge pull request #728 from bitovi/canjs-2.0

CanJS 2.0 architecture example
parents 20d346f5 1bc79b0a
......@@ -2,9 +2,9 @@
"name": "todomvc-canjs",
"version": "0.0.0",
"dependencies": {
"jquery": "~1.9.1",
"canjs": "~1.1.5",
"canjs-localstorage": "~0.1.0",
"jquery": "~2.0.0",
"canjs": "~2.0.0",
"canjs-localstorage": "~0.2.0",
"todomvc-common": "~0.1.6"
}
}
can.Model('can.Model.LocalStorage', {
// Implement local storage handling
localStore : function (cb) {
var name = this.name,
data = JSON.parse(window.localStorage[name] || (window.localStorage[name] = '[]')),
res = cb.call(this, data);
if (res !== false) {
can.each(data, function (todo) {
delete todo.editing;
});
window.localStorage[name] = JSON.stringify(data);
}
},
(function(define) {
if (typeof define == "undefined") {
define = function(deps, fn) {
can.Model.LocalStorage = fn(can.Model);
}
}
findAll : function (params) {
var def = new can.Deferred();
this.localStore(function (todos) {
var instances = [],
self = this;
can.each(todos, function (todo) {
instances.push(new self(todo));
});
def.resolve({data : instances});
});
return def;
},
define(['can/model'], function(Model) {
return Model.extend({
// Implement local storage handling
localStore: function(cb) {
var name = this.name,
data = JSON.parse(window.localStorage[name] || (window.localStorage[name] = '[]')),
res = cb.call(this, data);
if (res !== false) {
can.each(data, function(todo) {
delete todo.editing;
});
window.localStorage[name] = JSON.stringify(data);
}
},
destroy : function (id) {
var def = new can.Deferred();
this.localStore(function (todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todos.splice(i, 1);
break;
}
}
def.resolve({});
});
return def;
},
findAll: function(params) {
var def = new can.Deferred();
this.localStore(function(todos) {
var instances = [],
self = this;
can.each(todos, function(todo) {
instances.push(new self(todo));
});
def.resolve({data: instances});
});
return def;
},
create : function (attrs) {
var def = new can.Deferred();
this.localStore(function (todos) {
attrs.id = attrs.id || parseInt(100000 * Math.random(), 10);
todos.push(attrs);
});
def.resolve({id : attrs.id});
return def;
},
destroy: function(id) {
var def = new can.Deferred();
this.localStore(function(todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todos.splice(i, 1);
break;
}
}
def.resolve({});
});
return def;
},
update : function (id, attrs) {
var def = new can.Deferred(), todo;
this.localStore(function (todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todo = todos[i];
break;
}
}
can.extend(todo, attrs);
});
def.resolve({});
return def;
}
}, {});
\ No newline at end of file
create: function(attrs) {
var def = new can.Deferred();
this.localStore(function(todos) {
attrs.id = attrs.id || parseInt(100000 * Math.random(), 10);
todos.push(attrs);
});
def.resolve({id: attrs.id});
return def;
},
update: function(id, attrs) {
var def = new can.Deferred(), todo;
this.localStore(function(todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todo = todos[i];
break;
}
}
can.extend(todo, attrs);
});
def.resolve({});
return def;
}
}, {});
});
})(window.define);
......@@ -14,13 +14,51 @@
<p>Written by <a href="http://bitovi.com">Bitovi</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/jquery/jquery.js"></script>
<script type="text/mustache" id="app-template">
<todo-app>
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus="" can-enter="createTodo">
</header>
<section id="main" class="{{^if todos.length}}hidden{{/if}}">
<input id="toggle-all" type="checkbox" {{#if todos.allComplete}}checked="checked"{{/if}} can-click="toggleAll">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
{{#each displayList}}
<li class="todo{{#if complete}} completed{{/if}}{{#if editing}} editing{{/if}}">
<div class="view">
<input class="toggle" type="checkbox" can-value="complete">
<label can-dblclick="edit">{{text}}</label>
<button class="destroy" can-click="destroy"></button>
</div>
<input class="edit" type="text" value="{{text}}" can-blur="updateTodo"
can-keyup="cancelEditing" can-enter="updateTodo">
</li>
{{/each}}
</ul>
</section>
<footer id="footer" class="{{^if todos.length}}hidden{{/if}}">
<span id="todo-count">
<strong>{{todos.remaining}}</strong> {{plural "item" todos.remaining}} left
</span>
<ul id="filters">
<li>{{{link "All" undefined}}}</li>
<li>{{{link "Active" "active"}}}</li>
<li>{{{link "Completed" "completed"}}}</li>
</ul>
<button id="clear-completed" class="{{^if todos.completed.length}}hidden{{/if}}" can-click="clearCompleted">
Clear completed ({{todos.completed.length}})
</button>
</footer>
</todo-app>
</script>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/canjs/can.jquery.js"></script>
<script src="bower_components/canjs-localstorage/can.localstorage.js"></script>
<script src="js/lib/can.mustache.min.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/todos/todos.js"></script>
<script src="js/components/todo-app.js"></script>
<script src="js/app.js"></script>
</body>
</html>
/*global $ Todos Models Mustache can*/
/* global $, can */
(function () {
'use strict';
$(function () {
// Set up a route that maps to the `filter` attribute
can.route(':filter');
// Delay routing until we initialized everything
can.route.ready(false);
// View helper for pluralizing strings
Mustache.registerHelper('plural', function (str, count) {
return str + (count !== 1 ? 's' : '');
});
// Initialize the app
Models.Todo.findAll({}, function (todos) {
new Todos('#todoapp', {
todos: todos,
state: can.route,
view : 'views/todos.mustache'
});
});
// Render #app-template
$('#todoapp').html(can.view('app-template', {}));
// Now we can start routing
can.route.ready(true);
// Start the router
can.route.ready();
});
})();
/* global can */
(function (namespace) {
'use strict';
var ESCAPE_KEY = 27;
can.Component.extend({
// Create this component on a tag like `<todo-app>`.
tag: 'todo-app',
scope: {
// Store the Todo model in the scope
Todo: namespace.Models.Todo,
// A list of all Todos retrieved from LocalStorage
todos: new namespace.Models.Todo.List({}),
// Edit a Todo
edit: function (todo, el) {
todo.attr('editing', true);
el.parents('.todo').find('.edit').focus();
},
cancelEditing: function (todo, el, ev) {
if (ev.which === ESCAPE_KEY) {
el.val(todo.attr('text'));
todo.attr('editing', false);
}
},
// Returns a list of Todos filtered based on the route
displayList: function () {
var filter = can.route.attr('filter');
return this.todos.filter(function (todo) {
if (filter === 'completed') {
return todo.attr('complete');
}
if (filter === 'active') {
return !todo.attr('complete');
}
return true;
});
},
updateTodo: function (todo, el) {
var value = can.trim(el.val());
if (value === '') {
todo.destroy();
} else {
todo.attr({
editing: false,
text: value
});
}
},
createTodo: function (context, el) {
var value = can.trim(el.val());
var TodoModel = this.Todo;
if (value !== '') {
new TodoModel({
text: value,
complete: false
}).save();
can.route.removeAttr('filter');
el.val('');
}
},
toggleAll: function (scope, el) {
var toggle = el.prop('checked');
this.attr('todos').each(function (todo) {
todo.attr('complete', toggle);
});
},
clearCompleted: function () {
this.attr('todos').completed().forEach(function (todo) {
todo.destroy();
});
}
},
events: {
// When a new Todo has been created, add it to the todo list
'{Todo} created': function (Construct, ev, todo) {
this.scope.attr('todos').push(todo);
}
},
helpers: {
link: function (name, filter) {
var data = filter ? { filter: filter } : {};
return can.route.link(name, data, {
className: can.route.attr('filter') === filter ? 'selected' : ''
});
},
plural: function (singular, num) {
return num() === 1 ? singular : singular + 's';
}
}
});
})(this);
(function(i,m,l){i.view.ext=".mustache";var n=function(a){return i.isFunction(a.attr)&&a.constructor&&!!a.constructor.canMakeObserve},o=function(a){return a&&a.splice&&"number"==typeof a.length},h=function(a){if(this.constructor!=h){var c=new h(a);return function(a){return c.render(a)}}"function"==typeof a?this.template={fn:a}:(i.extend(this,a),this.template=this.scanner.scan(this.text,this.name))};i.Mustache=m.Mustache=h;h.prototype.render=function(a){a=a||{};return this.template.fn.call(a,a,{_data:a})};
i.extend(h.prototype,{scanner:new i.view.Scanner({text:{start:"var ___c0nt3xt = []; ___c0nt3xt.___st4ck = true;var ___st4ck = function(context, self) {var s;if (arguments.length == 1 && context) {s = !context.___st4ck ? [context] : context;} else {s = context && context.___st4ck ? context.concat([self]) : ___st4ck(context).concat([self]);}return (s.___st4ck = true) && s;};"},tokens:[["returnLeft","{{{","{{[{&]"],["commentFull","{{!}}","^[\\s\\t]*{{!.+?}}\\n"],["commentLeft","{{!","(\\n[\\s\\t]*{{!|{{!)"],
["escapeFull","{{}}","(^[\\s\\t]*{{[#/^][^}]+?}}\\n|\\n[\\s\\t]*{{[#/^][^}]+?}}\\n|\\n[\\s\\t]*{{[#/^][^}]+?}}$)",function(a){return{before:/^\n.+?\n$/.test(a)?"\n":"",content:a.match(/\{\{(.+?)\}\}/)[1]||""}}],["escapeLeft","{{"],["returnRight","}}}"],["right","}}"]],helpers:[{name:/^>[\s|\w]\w*/,fn:function(a){return"can.view.render('"+i.trim(a.replace(/^>\s?/,""))+"', ___st4ck(___c0nt3xt,this).pop())"}},{name:/^\s?data\s/,fn:function(a){return"can.proxy(function(__){can.data(can.$(__),'"+a.replace(/(^\s?data\s)|(["'])/g,
"")+"', this.pop()); }, ___st4ck(___c0nt3xt,this))"}},{name:/^.*$/,fn:function(a,c){var b=!1,d=[],a=i.trim(a);if(a.length&&(b=a.match(/^([#^/]|else$)/))){b=b[0];switch(b){case "#":case "^":d.push(c.insert+"can.view.txt(0,'"+c.tagName+"',"+c.status+",this,function(){ return ");break;case "/":return{raw:'return ___v1ew.join("");}}])}));'}}a=a.substring(1)}if("else"!=b){var j=[],f=0,g=!1,h,e;(i.trim(a)+" ").replace(/((([^\s]+?=)?('.*?'|".*?"))|.*?)\s/g,function(a,b){j.push(b)});for(d.push("can.Mustache.txt(___st4ck(___c0nt3xt,this),"+
(b?'"'+b+'"':"null")+",");h=j[f];f++)f&&d.push(","),f&&(e=h.match(/^(('.*?'|".*?"|[0-9.]+|true|false)|((.+?)=(('.*?'|".*?"|[0-9.]+|true|false)|(.+))))$/))?e[2]?d.push(e[0]):(g||(g=!0,d.push("{___h4sh:{")),d.push(e[4],":",e[6]?e[6]:'can.Mustache.get("'+e[5].replace(/"/g,'\\"')+'",___st4ck(___c0nt3xt,this))'),f==j.length-1&&d.push("}}")):d.push('can.Mustache.get("'+h.replace(/"/g,'\\"')+'",___st4ck(___c0nt3xt,this)'+(0==f&&1<j.length?",true":"")+")")}b&&"else"!=b&&d.push(",[{_:function(){");switch(b){case "#":d.push('return ___v1ew.join("");}},{fn:function(___c0nt3xt){var ___v1ew = [];');
break;case "else":case "^":d.push('return ___v1ew.join("");}},{inverse:function(___c0nt3xt){var ___v1ew = [];');break;default:d.push(");")}d=d.join("");return b?{raw:d}:d}}]})});for(var m=i.view.Scanner.prototype.helpers,p=0;p<m.length;p++)h.prototype.scanner.helpers.unshift(m[p]);h.registerHelper=function(a,c){this._helpers.push({name:a,fn:c})};h.getHelper=function(a){for(var c=0,b;b=this._helpers[c];c++)if(b.name==a)return b;return null};h.txt=function(a,c,b){var d=Array.prototype.slice.call(arguments,
3),j=i.extend.apply(i,[{fn:function(){},inverse:function(){}}].concat(c?d.pop():[])),f=d.length?d:[b],g=!0,k=[],e;if(c)for(e=0;e<f.length;e++)g=o(f[e])?"#"==c?g&&!!f[e].length:"^"==c?g&&!f[e].length:g:"#"==c?g&&!!f[e]:"^"==c?g&&!f[e]:g;if(f=h.getHelper(b)||i.isFunction(b)&&{fn:b}){a=a.___st4ck&&a[a.length-1]||a;j={fn:i.proxy(j.fn,a),inverse:i.proxy(j.inverse,a)};if((g=d[d.length-1])&&g.___h4sh)j.hash=d.pop().___h4sh;d.push(j);return f.fn.apply(a,d)||""}if(g)switch(c){case "#":if(o(b)){for(e=0;e<b.length;e++)k.push(j.fn.call(b[e]||
{},a)||"");return k.join("")}return j.fn.call(b||{},a)||"";case "^":return j.inverse.call(b||{},a)||"";default:return""+(b!==l?b:"")}return""};h.get=function(a,c,b){var d=a.split("."),j=c[c.length-1],f=c[c.length-2],g,k,e;if(/^\.|this$/.test(a)){if(/^object|undefined$/.test(typeof f)){for(;b=c.pop();)if("undefined"!==typeof b)return b;return""}return f||""}if(!b)for(f=c.length-1;0<=f;f--){b=c[f];if(b!==l)for(e=0;e<d.length;e++)if("undefined"!=typeof b[d[e]])g=b,b=b[k=d[e]];else{n(b)?(g=b,k=d[e]):
g=b=l;break}if(b!==l){if(i.isFunction(g[k]))return g[k]();if(n(g))return g.attr(k);n(b)&&o(b)&&b.attr("length");return b}}return j!==l&&i.isFunction(j[a])?j[a]:h.getHelper(a)?a:""};h._helpers=[{name:"if",fn:function(a,c){return a?c.fn(this):c.inverse(this)}},{name:"unless",fn:function(a,c){if(!a)return c.fn(this)}},{name:"each",fn:function(a,c){if(a&&a.length){for(var b=[],d=0;d<a.length;d++)b.push(c.fn(a[d]));return b.join("")}}},{name:"with",fn:function(a,c){if(a)return c.fn(a)}}];i.view.register({suffix:"mustache",
contentType:"x-mustache-template",script:function(a,c){return"can.Mustache(function(_CONTEXT,_VIEW) { "+(new h({text:c,name:a})).template.out+" })"},renderer:function(a,c){return h({text:c,name:a})}})})(can,this);
/*global can */
(function (namespace, undefined) {
(function (namespace) {
'use strict';
// Basic Todo entry model
// { text: 'todo', complete: false }
var Todo = can.Model.LocalStorage({
var Todo = can.Model.LocalStorage.extend({
storageName: 'todos-canjs'
}, {
// Returns if this instance matches a given filter
// (currently `active` and `complete`)
matches : function () {
var filter = can.route.attr('filter');
return !filter || (filter === 'active' && !this.attr('complete')) ||
(filter === 'completed' && this.attr('complete'));
init: function () {
// Autosave when changing the text or completing the todo
this.on('change', function (ev, prop) {
if (prop === 'text' || prop === 'complete') {
ev.target.save();
}
});
}
});
// List for Todos
Todo.List = can.Model.List({
completed: function () {
var completed = 0;
Todo.List = Todo.List.extend({
filter: function (check) {
var list = [];
this.each(function (todo) {
completed += todo.attr('complete') ? 1 : 0;
if (check(todo)) {
list.push(todo);
}
});
return completed;
return list;
},
completed: function () {
return this.filter(function (todo) {
return todo.attr('complete');
});
},
remaining: function () {
return this.attr('length') - this.completed();
return this.attr('length') - this.completed().length;
},
allComplete: function () {
return this.attr('length') === this.completed();
return this.attr('length') === this.completed().length;
}
});
......
/*global can Models*/
(function (namespace, undefined) {
'use strict';
var ENTER_KEY = 13;
var Todos = can.Control({
// Default options
defaults : {
view : 'views/todos.ejs'
}
}, {
// Initialize the Todos list
init: function () {
// Render the Todos
this.element.append(can.view(this.options.view, this.options));
},
// Listen for when a new Todo has been entered
'#new-todo keyup': function (el, e) {
var value = can.trim(el.val());
if (e.keyCode === ENTER_KEY && value !== '') {
new Models.Todo({
text : value,
complete : false
}).save(function () {
el.val('');
});
}
},
// Handle a newly created Todo
'{Models.Todo} created': function (list, e, item) {
this.options.todos.push(item);
// Reset the filter so that you always see your new todo
this.options.state.attr('filter', '');
},
// Listener for when the route changes
'{state} change' : function () {
// Remove the `selected` class from the old link and add it to the link for the current location hash
this.element.find('#filters').find('a').removeClass('selected')
.end().find('[href="' + window.location.hash + '"]').addClass('selected');
},
// Listen for editing a Todo
'.todo dblclick': function (el) {
el.data('todo').attr('editing', true).save(function () {
el.children('.edit').focus();
});
},
// Update a todo
updateTodo: function (el) {
var value = can.trim(el.val()),
todo = el.closest('.todo').data('todo');
// If we don't have a todo we don't need to do anything
if (!todo) {
return;
}
if (value === '') {
todo.destroy();
} else {
todo.attr({
editing : false,
text : value
}).save();
}
},
// Listen for an edited Todo
'.todo .edit keyup': function (el, e) {
if (e.keyCode === ENTER_KEY) {
this.updateTodo(el);
}
},
'.todo .edit focusout' : 'updateTodo',
// Listen for the toggled completion of a Todo
'.todo .toggle click': function (el) {
el.closest('.todo').data('todo')
.attr('complete', el.is(':checked'))
.save();
},
// Listen for a removed Todo
'.todo .destroy click': function (el) {
el.closest('.todo').data('todo').destroy();
},
// Listen for toggle all completed Todos
'#toggle-all click': function (el) {
var toggle = el.prop('checked');
can.each(this.options.todos, function (todo) {
todo.attr('complete', toggle).save();
});
},
// Listen for removing all completed Todos
'#clear-completed click': function () {
for (var i = this.options.todos.length - 1, todo; i > -1 && (todo = this.options.todos[i]); i--) {
if (todo.attr('complete')) {
todo.destroy();
}
}
}
});
namespace.Todos = Todos;
})(this);
......@@ -4,34 +4,35 @@
> _[CanJS - canjs.com](http://canjs.com)_
## Learning CanJS
The [CanJS website](http://canjs.com) is a great resource for getting started.
Here are some links you may find helpful:
* [Documentation](http://donejs.com/docs.html#!canjs)
* [Why CanJS](http://canjs.com/#why_canjs)
* [Applications built with CanJS](http://canjs.com/#examples)
* [CanJS guides documentation](http://canjs.com/guides/index.html)
* [API documentation](http://canjs.com/docs/index.html)
* [Blog](http://bitovi.com/blog/tag/canjs)
* [Getting started video](http://www.youtube.com/watch?v=GdT4Oq6ZQ68)
Articles and guides from the community:
* [Diving into CanJS](http://net.tutsplus.com/tutorials/javascript-ajax/diving-into-canjs)
Get help from other CanJS users:
* [CanJS on StackOverflow](http://stackoverflow.com/questions/tagged/canjs)
* [CanJS Forums](http://forum.javascriptmvc.com/#Forum/canjs)
* [CanJS on Twitter](http://twitter.com/canjs)
* [#canjs](http://webchat.freenode.net/?channels=canjs) IRC channel on Freenode
_If you have other helpful links to share, or find any of the links above no longer work, please [let us know](https://github.com/tastejs/todomvc/issues)._
## Implementation
The CanJS TodoMVC example uses [can.Component](http://canjs.com/guides/Components.html) introduced in CanJS 2.0.
can.Component supports declarative view bindings using Mustache/Handlebars as the template syntax.
Version 2 is mostly backwards compatible with previous 1.1.x version. For alternative architecture examples have a look at
the [TodoMVC 1.2.0 CanJS example](https://github.com/tastejs/todomvc/tree/1.2.0/architecture-examples/canjs).
### CanJS and JavaScriptMVC
CanJS is the extracted, more modern and more library-like MVC parts of [JavaScriptMVC](http://javascriptmvc.com), formerly known as jQueryMX.
......@@ -45,20 +46,3 @@ Additionally, JavaScriptMVC contains:
- [StealJS](http://javascriptmvc.com/docs.html#!stealjs) - A JavaScript package manager
- [DocumentJS](http://javascriptmvc.com/docs.html#!DocumentJS) - A documentation engine
- [FuncUnit](http://funcunit.com) - jQuery style functional unit testing
### View engines
CanJS supports both live binding [EJS](http://canjs.us/#can_ejs) and [Mustache/Handlebars](http://canjs.us/#can_mustache)
templates. By default the Mustache view will be used but an EJS example is available as well.
You can easily change it by modifying the `view` option in the `js/app.js` file:
```js
Models.Todo.findAll({}, function (todos) {
new Todos('#todoapp', {
todos: todos,
state: can.route,
view: 'views/todos.ejs'
});
});
```
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main" class="<%= todos.attr("length") === 0 ? "hidden" : "" %>">
<input id="toggle-all" type="checkbox" <%= todos.allComplete() ? "checked" : "" %>>
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<% todos.each(function( todo ) { %>
<li class="todo
<%= todo.matches(state.attr('filter')) ? '' : 'hidden' %>
<%= todo.attr('complete') ? 'completed' : '' %>
<%= todo.attr('editing') ? 'editing' : '' %>"
<%= (el)-> el.data('todo', todo) %>>
<div class="view">
<input class="toggle" type="checkbox" <%= todo.attr('complete') ? 'checked' : '' %>>
<label><%= todo.attr('text') %></label>
<button class="destroy"></button>
</div>
<input class="edit" value="<%= todo.attr('text') %>">
</li>
<% }) %>
</ul>
</section>
<footer id="footer" class="<%= todos.attr('length') === 0 ? 'hidden' : '' %>">
<span id="todo-count">
<strong><%= todos.remaining() %></strong>
item<%= todos.remaining() == 1 ? "" : "s" %> 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" class="<%= todos.completed() === 0 ? 'hidden' : '' %>">
Clear completed (<%= todos.completed() %>)
</button>
</footer>
<header id="header">
<h1>todos</h1>
<input id="new-todo" {{ (el) -> el.val('').focus() }} placeholder="What needs to be done?" autofocus="">
</header>
<section id="main" class="{{^todos}}hidden{{/todos}}">
<input id="toggle-all" type="checkbox" {{#todos.allComplete}}checked="checked"{{/todos.allComplete}}>
<label for="toggle-all" >Mark all as complete</label>
<ul id="todo-list">
{{#todos}}
<li class="todo {{^matches}}hidden{{/matches}} {{#complete}}completed{{/complete}} {{#editing}}editing{{/editing}}" {{data 'todo'}}>
<div class="view">
<input class="toggle" type="checkbox" {{#complete}}checked="checked"{{/complete}}>
<label>{{text}}</label>
<button class="destroy"></button>
</div>
<input class="edit" type="text" value="{{text}}">
</li>
{{/todos}}
</ul>
</section>
<footer id="footer" class="{{^todos}}hidden{{/todos}}">
<span id="todo-count">
<strong>{{todos.remaining}}</strong> {{plural "item" todos.remaining}} 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" class="{{^todos.completed}}hidden{{/todos.completed}}">
Clear completed ({{todos.completed}})
</button>
</footer>
......@@ -311,10 +311,10 @@
"heading": "Official Resources",
"links": [{
"name": "Documentation",
"url": "http://donejs.com/docs.html#!canjs"
"url": "http://canjs.com/docs/index.html"
}, {
"name": "Why CanJS",
"url": "http://canjs.com/#why_canjs"
"name": "Getting started",
"url": "http://canjs.com/guides/Tutorial.html"
}, {
"name": "Applications built with CanJS",
"url": "http://canjs.com/#examples"
......@@ -342,6 +342,9 @@
}, {
"name": "CanJS on Twitter",
"url": "http://twitter.com/canjs"
}, {
"name": "#canjs IRC",
"url": "http://webchat.freenode.net/?channels=canjs"
}]
}]
},
......
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