Commit 1bc79b0a authored by David Luecke's avatar David Luecke

CanJS 2.0 architecture example.

parent 11a31d38
......@@ -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', {
(function(define) {
if (typeof define == "undefined") {
define = function(deps, fn) {
can.Model.LocalStorage = fn(can.Model);
}
}
define(['can/model'], function(Model) {
return Model.extend({
// Implement local storage handling
localStore : function (cb) {
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) {
can.each(data, function(todo) {
delete todo.editing;
});
window.localStorage[name] = JSON.stringify(data);
}
},
findAll : function (params) {
findAll: function(params) {
var def = new can.Deferred();
this.localStore(function (todos) {
this.localStore(function(todos) {
var instances = [],
self = this;
can.each(todos, function (todo) {
can.each(todos, function(todo) {
instances.push(new self(todo));
});
def.resolve({data : instances});
def.resolve({data: instances});
});
return def;
},
destroy : function (id) {
destroy: function(id) {
var def = new can.Deferred();
this.localStore(function (todos) {
this.localStore(function(todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todos.splice(i, 1);
......@@ -39,19 +47,19 @@ can.Model('can.Model.LocalStorage', {
return def;
},
create : function (attrs) {
create: function(attrs) {
var def = new can.Deferred();
this.localStore(function (todos) {
this.localStore(function(todos) {
attrs.id = attrs.id || parseInt(100000 * Math.random(), 10);
todos.push(attrs);
});
def.resolve({id : attrs.id});
def.resolve({id: attrs.id});
return def;
},
update : function (id, attrs) {
update: function(id, attrs) {
var def = new can.Deferred(), todo;
this.localStore(function (todos) {
this.localStore(function(todos) {
for (var i = 0; i < todos.length; i++) {
if (todos[i].id === id) {
todo = todos[i];
......@@ -63,4 +71,6 @@ can.Model('can.Model.LocalStorage', {
def.resolve({});
return def;
}
}, {});
\ No newline at end of file
}, {});
});
})(window.define);
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -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