Commit f9552feb authored by Tom Dale's avatar Tom Dale Committed by Sindre Sorhus

Bring Ember example up-to-date with 1.0

Closes #407
Fixes #394
parent 8632a28c
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ember.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ember.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<!--[if IE]>
<script src="../../assets/ie.js"></script>
<![endif]-->
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>
Created by
<a href="http://github.com/tomdale">Tom Dale</a>,
<a href="http://github.com/addyosmani">Addy Osmani</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<!-- /* Handlebars templates start */ -->
<script id="statsTemplate" type="text/x-handlebars">
{{#with view}}
{{#if oneLeft }}
<strong>{{entries.remaining}}</strong> item left
{{else}}
<strong>{{entries.remaining}}</strong> items left
{{/if}}
{{/with}}
</script>
<script id="filtersTemplate" type="text/x-handlebars">
<ul id="filters">
<li>
<a {{action showAll href=true}} {{bindAttr class="view.isAll:selected"}}>All</a>
</li>
<li>
<a {{action showActive href=true}} {{bindAttr class="view.isActive:selected"}}>Active</a>
</li>
<li>
<a {{action showCompleted href=true}} {{bindAttr class="view.isCompleted:selected"}}>Completed</a>
</li>
</ul>
</script>
<script id="clearBtnTemplate" type="text/x-handlebars">
{{#with view}}
<button {{action "clearCompleted" target="entries"}} {{bindAttr class="buttonClass:hidden"}} >
Clear completed ({{entries.completed}})
</button>
{{/with}}
</script>
<script id="todosTemplate" type="text/x-handlebars">
{{#unless view.content.editing}}
{{view Ember.Checkbox checkedBinding="view.content.completed" class="toggle"}}
<label>{{view.content.title}}</label>
<button {{action removeItem target="this"}} class="destroy" ></button>
{{else}}
{{view view.ItemEditorView contentBinding="view.content"}}
{{/unless}}
</script>
<script>
// Workaround until Ember 1.0 is released
var ENV = {'FORCE_JQUERY': true};
</script>
<!-- /* Handlebars templates end */ -->
<script src="../../assets/base.js"></script>
<script src="../../assets/jquery.min.js"></script>
<script src="../../assets/handlebars.min.js"></script>
<script src="js/libs/ember-latest.js"></script>
<script src="js/app.js"></script>
<script src="js/router.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/models/store.js"></script>
<script src="js/controllers/entries.js"></script>
<script src="js/controllers/todos.js"></script>
<script src="js/views/application.js"></script>
<script src="js/views/todos.js"></script>
<script type="text/x-handlebars" data-template-name="todos">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
{{view Ember.TextField id="new-todo" placeholder="What needs to be done?"
valueBinding="newTitle" action="createTodo"}}
</header>
{{#if length}}
<section id="main">
<ul id="todo-list">
{{#each filteredTodos itemController="todo"}}
<li {{bindAttr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
{{view Todos.EditTodoView todoBinding="this"}}
{{else}}
{{view Ember.Checkbox checkedBinding="isCompleted" class="toggle"}}
<label {{action "editTodo" on="doubleClick"}}>{{title}}</label>
<button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>
{{/each}}
</ul>
{{view Ember.Checkbox id="toggle-all" checkedBinding="allAreDone"}}
</section>
<footer id="footer">
<span id="todo-count">{{{remainingFormatted}}}</span>
<ul id="filters">
<li>
{{#linkTo todos.index activeClass="selected"}}All{{/linkTo}}
</li>
<li>
{{#linkTo todos.active activeClass="selected"}}Active{{/linkTo}}
</li>
<li>
{{#linkTo todos.completed activeClass="selected"}}Completed{{/linkTo}}
</li>
</ul>
{{#if hasCompleted}}
<button id="clear-completed" {{action "clearCompleted"}} {{bindAttr class="buttonClass:hidden"}}>
Clear completed ({{completed}})
</button>
{{/if}}
</footer>
{{/if}}
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>
Created by
<a href="http://github.com/tomdale">Tom Dale</a>,
<a href="http://github.com/addyosmani">Addy Osmani</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</script>
<!-- /* Handlebars templates end */ -->
<script src="../../assets/base.js"></script>
<script src="../../assets/jquery.min.js"></script>
<script src="js/libs/handlebars-1.0.rc.2.js"></script>
<script src="js/libs/ember.js"></script>
<script src="js/libs/ember-data.js"></script>
<script src="js/libs/local_storage_adapter.js"></script>
<script src="js/app.js"></script>
<script src="js/router.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/models/store.js"></script>
<script src="js/controllers/todos_controller.js"></script>
<script src="js/controllers/todo_controller.js"></script>
<script src="js/views/todo_view.js"></script>
<script src="js/views/edit_todo_view.js"></script>
</body>
</html>
(function( win ) {
'use strict';
win.Todos = Ember.Application.create({
VERSION: '1.0',
rootElement: '#todoapp',
storeNamespace: 'todos-emberjs',
// Extend to inherit outlet support
ApplicationController: Ember.Controller.extend(),
ready: function() {
this.initialize();
}
});
})( window );
window.Todos = Ember.Application.create();
(function( app ) {
'use strict';
var Entries = Ember.ArrayProxy.extend({
store: new app.Store( app.storeNamespace ),
content: [],
createNew: function( value ) {
if ( !value.trim() )
return;
var todo = this.get( 'store' ).createFromTitle( value );
this.pushObject( todo );
},
pushObject: function( item, ignoreStorage) {
if ( !ignoreStorage )
this.get( 'store' ).create( item );
return this._super( item );
},
removeObject: function( item ) {
this.get( 'store' ).remove( item );
return this._super( item );
},
clearCompleted: function() {
this.filterProperty(
'completed', true
).forEach( this.removeObject, this );
},
total: function() {
return this.get( 'length' );
}.property( '@each.length' ),
remaining: function() {
return this.filterProperty( 'completed', false ).get( 'length' );
}.property( '@each.completed' ),
completed: function() {
return this.filterProperty( 'completed', true ).get( 'length' );
}.property( '@each.completed' ),
noneLeft: function() {
return this.get( 'total' ) === 0;
}.property( 'total' ),
allAreDone: function( key, value ) {
if ( value !== undefined ) {
this.setEach( 'completed', value );
return value;
} else {
return !!this.get( 'length' ) &&
this.everyProperty( 'completed', true );
}
}.property( '@each.completed' ),
init: function() {
this._super();
// Load items if any upon initialization
var items = this.get( 'store' ).findAll();
if ( items.get( 'length' ) ) {
this.set( '[]', items );
};
}
});
app.EntriesController = Entries;
app.entriesController = Entries.create();
})( window.Todos );
Todos.TodoController = Ember.ObjectController.extend({
isEditing: false,
editTodo: function() {
this.set('isEditing', true);
},
removeTodo: function() {
var todo = this.get('model');
todo.deleteRecord();
todo.get('store').commit();
}
});
(function( app ) {
'use strict';
var TodosController = Ember.Controller.extend({
entries: function() {
var filter = this.getPath( 'content.filterBy' );
if ( Ember.empty( filter ) ) {
return this.get( 'content' );
}
if ( !Ember.compare( filter, 'completed' ) ) {
return this.get( 'content' ).filterProperty( 'completed', true );
}
if ( !Ember.compare( filter, 'active' ) ) {
return this.get( 'content' ).filterProperty( 'completed', false );
}
}.property( 'content.remaining', 'content.filterBy' )
});
app.TodosController = TodosController;
})( window.Todos );
Todos.TodosController = Ember.ArrayController.extend({
createTodo: function() {
// Get the todo title set by the "New Todo" text field
var title = this.get('newTitle');
if (!title.trim()) { return; }
// Create the new Todo model
Todos.Todo.createRecord({
title: title,
isCompleted: false
});
// Clear the "New Todo" text field
this.set('newTitle', '');
// Save the new model
this.get('store').commit();
},
clearCompleted: function() {
var completed = this.filterProperty('isCompleted', true);
completed.invoke('deleteRecord');
this.get('store').commit();
},
remaining: function() {
return this.filterProperty( 'isCompleted', false ).get( 'length' );
}.property( '@each.isCompleted' ),
remainingFormatted: function() {
var remaining = this.get('remaining');
var plural = remaining === 1 ? 'item' : 'items';
return '<strong>%@</strong> %@ left'.fmt(remaining, plural);
}.property('remaining'),
completed: function() {
return this.filterProperty('isCompleted', true).get('length');
}.property('@each.isCompleted'),
hasCompleted: function() {
return this.get('completed') > 0;
}.property('completed'),
allAreDone: function( key, value ) {
if ( value !== undefined ) {
this.setEach( 'isCompleted', value );
return value;
} else {
return !!this.get( 'length' ) &&
this.everyProperty( 'isCompleted', true );
}
}.property( '@each.isCompleted' )
});
This diff is collapsed.
This diff is collapsed.
DS.LSSerializer = DS.JSONSerializer.extend({
addBelongsTo: function(data, record, key, association) {
data[key] = record.get(key + '.id');
},
addHasMany: function(data, record, key, association) {
data[key] = record.get(key).map(function(record) {
return record.get('id');
});
},
// extract expects a root key, we don't want to save all these keys to
// localStorage so we generate the root keys here
extract: function(loader, json, type, record) {
this._super(loader, this.rootJSON(json, type), type, record);
},
extractMany: function(loader, json, type, records) {
this._super(loader, this.rootJSON(json, type, 'pluralize'), type, records);
},
rootJSON: function(json, type, pluralize) {
var root = this.rootForType(type);
if (pluralize == 'pluralize') { root = this.pluralize(root); }
var rootedJSON = {};
rootedJSON[root] = json;
return rootedJSON;
}
});
DS.LSAdapter = DS.Adapter.extend(Ember.Evented, {
init: function() {
this._loadData();
},
generateIdForRecord: function() {
return Math.random().toString(32).slice(2).substr(0,5);
},
serializer: DS.LSSerializer.create(),
find: function(store, type, id) {
var namespace = this._namespaceForType(type);
this._async(function(){
var copy = Ember.copy(namespace.records[id]);
this.didFindRecord(store, type, copy, id);
});
},
findMany: function(store, type, ids) {
var namespace = this._namespaceForType(type);
this._async(function(){
var results = [];
for (var i = 0; i < ids.length; i++) {
results.push(Ember.copy(namespace.records[ids[i]]));
}
this.didFindMany(store, type, results);
});
},
// Supports queries that look like this:
//
// {
// <property to query>: <value or regex (for strings) to match>,
// ...
// }
//
// Every property added to the query is an "AND" query, not "OR"
//
// Example:
//
// match records with "complete: true" and the name "foo" or "bar"
//
// { complete: true, name: /foo|bar/ }
findQuery: function(store, type, query, recordArray) {
var namespace = this._namespaceForType(type);
this._async(function() {
var results = this.query(namespace.records, query);
this.didFindQuery(store, type, results, recordArray);
});
},
query: function(records, query) {
var results = [];
var id, record, property, test, push;
for (id in records) {
record = records[id];
for (property in query) {
test = query[property];
push = false;
if (Object.prototype.toString.call(test) == '[object RegExp]') {
push = test.test(record[property]);
} else {
push = record[property] === test;
}
}
if (push) {
results.push(record);
}
}
return results;
},
findAll: function(store, type) {
var namespace = this._namespaceForType(type);
this._async(function() {
var results = [];
for (var id in namespace.records) {
results.push(Ember.copy(namespace.records[id]));
}
this.didFindAll(store, type, results);
});
},
createRecords: function(store, type, records) {
var namespace = this._namespaceForType(type);
records.forEach(function(record) {
this._addRecordToNamespace(namespace, record);
}, this);
this._async(function() {
this._didSaveRecords(store, type, records);
});
},
updateRecords: function(store, type, records) {
var namespace = this._namespaceForType(type);
this._async(function() {
records.forEach(function(record) {
var id = record.get('id');
namespace.records[id] = record.serialize({includeId:true});
}, this);
this._didSaveRecords(store, type, records);
});
},
deleteRecords: function(store, type, records) {
var namespace = this._namespaceForType(type);
this._async(function() {
records.forEach(function(record) {
var id = record.get('id');
delete namespace.records[id];
});
this._didSaveRecords(store, type, records);
});
},
dirtyRecordsForHasManyChange: function(dirtySet, parent, relationship) {
dirtySet.add(parent);
},
dirtyRecordsForBelongsToChange: function(dirtySet, child, relationship) {
dirtySet.add(child);
},
// private
_getNamespace: function() {
return this.namespace || 'DS.LSAdapter';
},
_loadData: function() {
var storage = localStorage.getItem(this._getNamespace());
this._data = storage ? JSON.parse(storage) : {};
},
_didSaveRecords: function(store, type, records) {
var success = this._saveData();
if (success) {
store.didSaveRecords(records);
} else {
records.forEach(function(record) {
store.recordWasError(record);
});
this.trigger('QUOTA_EXCEEDED_ERR', records);
}
},
_saveData: function() {
try {
localStorage.setItem(this._getNamespace(), JSON.stringify(this._data));
return true;
} catch(error) {
if (error.name == 'QUOTA_EXCEEDED_ERR') {
return false;
} else {
throw new Error(error);
}
}
},
_namespaceForType: function(type) {
var namespace = type.url || type.toString();
return this._data[namespace] || (
this._data[namespace] = {records: {}}
);
},
_addRecordToNamespace: function(namespace, record) {
var data = record.serialize({includeId: true});
namespace.records[data.id] = data;
},
_async: function(callback) {
var _this = this;
setTimeout(function(){
Ember.run(_this, callback);
}, 1);
}
});
(function( app ) {
'use strict';
var Store = function( name ) {
this.name = name;
var store = localStorage.getItem( this.name );
this.data = ( store && JSON.parse( store ) ) || {};
// Save the current state of the **Store** to *localStorage*.
this.save = function() {
localStorage.setItem( this.name, JSON.stringify( this.data ) );
};
// Wrapper around `this.create`
// Creates a `Todo` model object out of the title
this.createFromTitle = function( title ) {
var todo = app.Todo.create({
title: title,
store: this
});
this.create( todo );
return todo;
};
// Store the model inside the `Store`
this.create = function ( model ) {
if ( !model.get( 'id' ) )
model.set( 'id', Date.now() );
return this.update( model );
};
// Update a model by replacing its copy in `this.data`.
this.update = function( model ) {
this.data[ model.get( 'id' ) ] = model.getProperties(
'id', 'title', 'completed'
);
this.save();
return model;
};
// Retrieve a model from `this.data` by id.
this.find = function( model ) {
var todo = app.Todo.create( this.data[ model.get( 'id' ) ] );
todo.set( 'store', this );
return todo;
};
// Return the array of all models currently in storage.
this.findAll = function() {
var result = [],
key;
for ( key in this.data ) {
var todo = app.Todo.create( this.data[ key ] );
todo.set( 'store', this );
result.push( todo );
}
return result;
};
// Delete a model from `this.data`, returning it.
this.remove = function( model ) {
delete this.data[ model.get( 'id' ) ];
this.save();
return model;
};
};
app.Store = Store;
})( window.Todos );
Todos.Store = DS.Store.extend({
revision: 11,
adapter: 'Todos.LSAdapter'
});
Todos.LSAdapter = DS.LSAdapter.extend({
namespace: 'todos-emberjs'
});
(function( app ) {
'use strict';
Todos.Todo = DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean'),
app.Todo = Ember.Object.extend({
id: null,
title: null,
completed: false,
// set store reference upon creation instead of creating static bindings
store: null,
// Observer that will react on item change and will update the storage
todoChanged: function() {
this.get( 'store' ).update( this );
}.observes( 'title', 'completed' )
});
})( window.Todos);
todoDidChange: function() {
Ember.run.once(this, function() {
this.get('store').commit();
});
}.observes('isCompleted', 'title')
});
(function( app ) {
'use strict';
var Router = Ember.Router.extend({
root: Ember.Route.extend({
showAll: Ember.Route.transitionTo( 'index' ),
showActive: Ember.Route.transitionTo( 'active' ),
showCompleted: Ember.Route.transitionTo( 'completed' ),
index: Ember.Route.extend({
route: '/',
connectOutlets: function( router ) {
var controller = router.get( 'applicationController' );
var context = app.entriesController;
context.set( 'filterBy', '' );
controller.connectOutlet( 'todos', context )
}
}),
active: Ember.Route.extend({
route: '/active',
connectOutlets: function( router ) {
var controller = router.get( 'applicationController' );
var context = app.entriesController;
context.set( 'filterBy', 'active' );
controller.connectOutlet( 'todos', context )
}
}),
completed: Ember.Route.extend({
route: '/completed',
connectOutlets: function( router ) {
var controller = router.get( 'applicationController' );
var context = app.entriesController;
context.set( 'filterBy', 'completed' );
controller.connectOutlet( 'todos', context )
}
}),
specs: Ember.Route.extend({
route: '/specs',
connectOutlets: function() {
// TODO: Write them
}
})
})
Todos.Router.map(function() {
this.resource('todos', { path: '/' }, function() {
this.route('active');
this.route('completed');
});
app.Router = Router;
})( window.Todos );
});
Todos.TodosRoute = Ember.Route.extend({
model: function() {
return Todos.Todo.find();
}
});
Todos.TodosIndexRoute = Ember.Route.extend({
setupController: function() {
var todos = Todos.Todo.find();
this.controllerFor('todos').set('filteredTodos', todos);
}
});
Todos.TodosActiveRoute = Ember.Route.extend({
setupController: function() {
var todos = Todos.Todo.filter(function(todo) {
if (!todo.get('isCompleted')) { return true; }
});
this.controllerFor('todos').set('filteredTodos', todos);
}
});
Todos.TodosCompletedRoute = Ember.Route.extend({
setupController: function() {
var todos = Todos.Todo.filter(function(todo) {
if (todo.get('isCompleted')) { return true; }
});
this.controllerFor('todos').set('filteredTodos', todos);
}
});
(function( app ) {
'use strict';
var ApplicationView = Ember.ContainerView.extend({
childViews: [ 'headerView', 'mainView', 'footerView' ],
headerView: Ember.ContainerView.create({
childViews: [ 'titleView', 'createTodoView' ],
elementId: 'header',
tagName: 'header',
titleView: Ember.View.create({
tagName: 'h1',
template: function() {
return 'todos';
}
}),
createTodoView: Ember.TextField.create({
entriesBinding: 'controller.namespace.entriesController',
placeholder: 'What needs to be done?',
elementId: 'new-todo',
insertNewline: function() {
var value = this.get( 'value' );
if ( value ) {
this.get( 'entries' ).createNew( value );
this.set( 'value', '' );
}
}
}),
}),
mainView: Em.ContainerView.create({
elementId: 'main',
tagName: 'section',
visibilityBinding: 'controller.namespace.entriesController.noneLeft',
classNameBindings: [ 'visibility:hidden' ],
childViews: [ 'outletView', 'markAllChkbox' ],
outletView: Ember.View.create({
template: Ember.Handlebars.compile( '{{outlet}}' ),
}),
markAllChkbox: Ember.Checkbox.create({
entriesBinding: 'controller.namespace.entriesController',
elementId: 'toggle-all',
checkedBinding: 'entries.allAreDone'
})
}),
footerView: Ember.ContainerView.create({
elementId: 'footer',
tagName: 'footer',
visibilityBinding: 'controller.namespace.entriesController.noneLeft',
classNameBindings: [ 'visibility:hidden' ],
childViews: [ 'statsView', 'filtersView', 'clearBtnView' ],
statsView: Ember.View.create({
entriesBinding: 'controller.namespace.entriesController',
elementId: 'todo-count',
tagName: 'span',
templateName: 'statsTemplate',
oneLeft: function() {
return this.getPath( 'entries.remaining' ) === 1;
}.property( 'entries.remaining' )
}),
filtersView: Ember.View.create({
templateName: 'filtersTemplate',
filterBinding: 'controller.namespace.entriesController.filterBy',
isAll: function() {
return Ember.empty( this.get('filter') );
}.property( 'filter' ),
isActive: function() {
return this.get('filter') === 'active';
}.property('filter'),
isCompleted: function() {
return this.get('filter') === 'completed';
}.property('filter')
}),
clearBtnView: Ember.View.create({
entriesBinding: 'controller.namespace.entriesController',
templateName: 'clearBtnTemplate',
elementId: 'clear-completed',
buttonClass: function () {
return !this.getPath( 'entries.completed' );
}.property( 'entries.completed' )
})
})
});
app.ApplicationView = ApplicationView;
})( window.Todos);
Todos.EditTodoView = Ember.TextField.extend({
classNames: ['edit'],
valueBinding: 'todo.title',
change: function() {
var value = this.get('value');
if (Ember.isEmpty(value)) {
this.get('controller').removeTodo();
}
},
focusOut: function() {
this.set('controller.isEditing', false);
},
insertNewline: function() {
this.set('controller.isEditing', false);
},
didInsertElement: function() {
this.$().focus();
}
});
Todos.TodoView = Ember.View.extend({
tagName: 'li',
classNameBindings: ['todo.isCompleted:completed', 'isEditing:editing'],
doubleClick: function(event) {
this.set('isEditing', true);
}
});
(function( app ) {
'use strict';
var TodosView = Ember.CollectionView.extend({
contentBinding: 'controller.entries',
tagName: 'ul',
elementId: 'todo-list',
itemViewClass: Ember.View.extend({
templateName: 'todosTemplate',
classNames: [ 'view' ],
classNameBindings: ['content.completed', 'content.editing'],
doubleClick: function() {
this.get( 'content' ).set( 'editing', true );
},
removeItem: function() {
this.getPath( 'controller.content' ).removeObject(
this.get( 'content' )
);
},
ItemEditorView: Ember.TextField.extend({
valueBinding: 'content.title',
classNames: [ 'edit' ],
change: function() {
if ( Ember.empty( this.getPath( 'content.title' ) ) ) {
this.getPath( 'controller.content' ).removeObject(
this.get( 'content' )
);
}else{
this.get('content').set('title', this.getPath('content.title').trim());
}
},
whenDone: function() {
this.get( 'content' ).set( 'editing', false );
},
focusOut: function() {
this.whenDone();
},
didInsertElement: function() {
this.$().focus();
},
insertNewline: function() {
this.whenDone();
}
})
})
});
app.TodosView = TodosView;
})( window.Todos);
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