Commit 1b653228 authored by Stas SUȘCOV's avatar Stas SUȘCOV Committed by Sindre Sorhus

Close GH-116: Ember.js + Require.js example.

parent 78a05954
# Ember.js + Require.js • [TodoMVC](http://todomvc.com)
## Running tests
To fire specs runner, append `#specs` to the url in address bar,
and reload the webpage.
## Credit
Initial release by @tomdale.
Refactoring and maintenance by @stas.
/* Helpers */
.hidden {
display: none
}
/* Mocha */
#mocha h1, h2 {
margin: 0;
}
#mocha h1 {
font-size: 1em;
font-weight: 200;
}
#mocha .suite .suite h1 {
font-size: .8em;
}
#mocha h2 {
font-size: 12px;
font-weight: normal;
cursor: pointer;
}
#mocha .suite {
margin-left: 15px;
}
#mocha .test {
margin-left: 15px;
}
#mocha .test:hover h2::after {
position: relative;
top: 0;
right: -10px;
content: '(view source)';
font-size: 12px;
font-family: arial;
color: #888;
}
#mocha .test.pending:hover h2::after {
content: '(pending)';
font-family: arial;
}
#mocha .test.pass::before {
content: '✓';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #00c41c;
}
#mocha .test.pending {
color: #0b97c4;
}
#mocha .test.pending::before {
content: '◦';
color: #0b97c4;
}
#mocha .test.fail {
color: #c00;
}
#mocha .test.fail pre {
color: black;
}
#mocha .test.fail::before {
content: '✖';
font-size: 12px;
display: block;
float: left;
margin-right: 5px;
color: #c00;
}
#mocha .test pre.error {
color: #c00;
}
#mocha .test pre {
display: inline-block;
font: 12px/1.5 monaco, monospace;
margin: 5px;
padding: 15px;
border: 1px solid #eee;
border-bottom-color: #ddd;
-webkit-border-radius: 3px;
-webkit-box-shadow: 0 1px 3px #eee;
}
#error {
color: #c00;
font-size: 1.5 em;
font-weight: 100;
letter-spacing: 1px;
}
#stats {
position: fixed;
top: 15px;
right: 10px;
font-size: 12px;
margin: 0;
color: #888;
}
#stats .progress {
float: right;
padding-top: 0;
}
#stats em {
color: black;
}
#stats li {
display: inline-block;
margin: 0 5px;
list-style: none;
padding-top: 11px;
}
code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>ember.js + require.js • TodoMVC</title>
<link rel="stylesheet" href="../../assets/base.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
</header>
<section id="main"></section>
<footer id="footer"></footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<p>
Created by <a href="http://github.com/tomdale/">Tom Dale</a>,
<a href="http://github.com/stas/">Стас Сушков</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script data-main="js/app.js" src="js/lib/require/require.js"></script>
</body>
</html>
// Define libraries
require.config({
baseUrl: 'js/',
paths: {
jquery: '../../../assets/jquery.min',
ember: 'lib/ember-0.9.7.1.min',
text: 'lib/require/text',
mocha: 'lib/mocha',
chai: 'lib/chai'
}
});
// Load our app
define( 'app', [
'jquery',
'app/models/store',
'app/controllers/todos',
'ember',
], function( $, Store, TodosController ) {
var App = Ember.Application.create({
VERSION: '0.2-omfg',
// Sets up mocha to run some integration tests
specsRunner: function( chai ) {
// Create placeholder for mocha output
// TODO: Make this shit look better and inside body
$( document.body ).before( '<div id="mocha"></div>' );
// Setup mocha and expose chai matchers
window.expect = chai.expect;
mocha.setup('bdd');
// Load testsuite
require([
'app/specs/models/store',
'app/specs/views/basic_acceptance',
'app/specs/controllers/todos'
], function() {
mocha.run().globals( [ '$', 'Ember', 'Todos' ] );
}
);
},
// Constructor
init: function() {
this._super();
// Initiate main controller
this.set(
'todosController',
TodosController.create({
store: new Store( 'todos-emberjs' )
})
);
// Run specs if asked
if ( location.hash.match( /specs/ ) ) {
require( [ 'chai', 'mocha' ], this.specsRunner );
}
}
});
// Expose the application globally
return window.Todos = App;
}
);
define('app/controllers/entries', ['ember'],
/**
* Entries controller
*
* @returns Class
*/
function() {
return Ember.ArrayProxy.extend({
store: null,
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 ) {
item = item.get( 'todo' ) || 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' ),
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 );
};
}
});
}
);
define('app/controllers/todos', [
'app/controllers/entries',
'text!app/views/clear_button.html',
'text!app/views/items.html'
],
/**
* Todos controller
*
* Main controller inherits the `Entries` class
* which is an `ArrayProxy` linked with the `Store` model
*
* @param Class Entries, the Entries class
* @param String button_html, the html view for the clearCompletedButton
* @param String items_html, the html view for the `Todos` items
* @returns Class
*/
function( Entries, button_html, items_html ) {
return Entries.extend({
// New todo input
inputView: Ember.TextField.create({
placeholder: 'What needs to be done?',
elementId: 'new-todo',
storageBinding: 'Todos.todosController',
// Bind this to newly inserted line
insertNewline: function() {
var value = this.get( 'value' );
if ( value ) {
this.get( 'storage' ).createNew( value );
this.set( 'value', '' );
}
}
}),
// Stats report
statsView: Ember.View.create({
elementId: 'todo-count',
tagName: 'span',
contentBinding: 'Todos.todosController',
remainingBinding: 'Todos.todosController.remaining',
template: Ember.Handlebars.compile(
'<strong>{{remaining}}</strong> {{remainingString}} left'
),
remainingString: function() {
var remaining = this.get( 'remaining' );
return ( remaining === 1 ? ' item' : ' items' );
}.property( 'remaining' )
}),
// Handle visibility of some elements as items totals change
visibilityObserver: function() {
$( '#main, #footer' ).toggle( !!this.get( 'total' ) );
}.observes( 'total' ),
// Clear completed tasks button
clearCompletedButton: Ember.Button.create({
template: Ember.Handlebars.compile( button_html ),
target: 'Todos.todosController',
action: 'clearCompleted',
completedCountBinding: 'Todos.todosController.completed',
elementId: 'clear-completed',
classNameBindings: 'buttonClass',
// Observer to update class if completed value changes
buttonClass: function () {
if ( !this.get( 'completedCount' ) )
return 'hidden';
}.property( 'completedCount' )
}),
// Checkbox to mark all todos done.
allDoneCheckbox: Ember.Checkbox.create({
elementId: 'toggle-all',
checkedBinding: 'Todos.todosController.allAreDone'
}),
// Compile and render the todos view
todosView: Ember.View.create({
template: Ember.Handlebars.compile( items_html )
}),
// Todo list item view
todoView: Ember.View.extend({
classNames: [ 'view' ],
doubleClick: function() {
this.get( 'content' ).set( 'editing', true );
}
}),
// Todo list item editing view
todoEditor: Ember.TextField.extend({
storageBinding: 'Todos.todosController',
classNames: [ 'edit' ],
whenDone: function() {
this.get( 'todo' ).set( 'editing', false );
if ( !this.get( 'todo' ).get( 'title' ).trim() ) {
this.get( 'storage' ).removeObject( this.get( 'todo' ) );
}
},
focusOut: function() {
this.whenDone();
},
didInsertElement: function() {
this.$().focus();
},
insertNewline: function() {
this.whenDone();
}
}),
// Activates the views and other initializations
init: function() {
this._super();
this.get( 'inputView' ).appendTo( 'header' );
this.get( 'allDoneCheckbox' ).appendTo( '#main' );
this.get( 'todosView' ).appendTo( '#main' );
this.get( 'statsView' ).appendTo( '#footer' );
this.get( 'clearCompletedButton' ).appendTo( '#footer' );
}
});
}
);
define('app/models/store', [
'app/models/todo',
'ember'
],
/**
* Todo entries storage model
*
* @param Class Todo, the todo entry model
* @returns Class
*/
function( Todo ) {
// Our Store is represented by a single JS object in *localStorage*.
// Create it with a meaningful name, like the name you'd give a table.
return 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 = 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 = 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 = 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;
};
};
}
);
define('app/models/todo', ['ember'],
/**
* Todo entry model
*
* @returns Class
*/
function() {
return 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' )
});
}
);
/**
* Some smoke tests
*/
describe( 'controllers/todos', function() {
var controller = Todos.get( 'todosController' );
var title = 'Another title...';
it( 'should have an input for entering new entry', function() {
expect( controller.inputView ).to.be.a( 'object' );
expect(
$( controller.inputView.get( 'element' ) ).attr( 'placeholder' )
).to.equal( controller.inputView.get( 'placeholder' ) );
});
it( 'should not create new entry on empty-ish input', function() {
var counted = controller.get( 'remaining' );
controller.inputView.set( 'value', ' ' );
controller.inputView.insertNewline();
expect( controller.get( 'remaining' ) ).to.equal( counted )
});
it( 'should create new entry on newline', function() {
controller.inputView.set( 'value', title );
controller.inputView.insertNewline();
expect( controller.get( 'lastObject' ).title ).to.equal( title );
controller.removeObject( controller.get( 'lastObject' ) );
});
it( 'should delete item if title is empty-ish', function() {
controller.createNew( title );
var counted = controller.get( 'remaining' );
var entry = controller.get( 'lastObject' );
entry.set( 'title', ' ' );
var editor = controller.todoEditor.create({
todo: entry,
storage: controller
});
editor.whenDone();
expect( controller.get( 'remaining' ) ).to.equal( counted - 1 );
});
it( 'should reflect the same number of items as in store', function() {
controller.createNew( 'value', title );
var visibles = controller.todosView.
get( 'childViews' )[ 0 ].get( 'childViews' ).length;
expect( controller.get( 'content' ).length ).to.equal( visibles );
controller.removeObject( controller.get( 'lastObject' ) );
});
it( 'should allow removing entries', function( done ) {
controller.createNew( 'value', title );
setTimeout( function(){
controller.allDoneCheckbox.set( 'value', true );
}, 100 );
setTimeout( function(){
controller.clearCompletedButton.triggerAction();
}, 200 );
setTimeout( function(){
expect( controller.get( 'content' ).length ).to.equal( 0 );
}, 300 );
done();
});
});
/**
* Some integration tests
*/
describe( 'models/store', function() {
var title = 'Testing title...';
var store = Todos.todosController.get( 'store' );
it( 'should allow creating and removing items', function() {
var count = store.findAll().length;
var todo = store.createFromTitle( title );
expect( store.findAll().length ).to.equal( count + 1 );
expect( todo ).to.have.property( 'title', title );
expect( todo ).to.have.property( 'completed', false );
expect( todo ).to.have.property( 'store', store );
store.remove( todo );
expect( store.findAll().length ).to.equal( count );
});
it( 'should allow finding and changing items', function() {
var todo = store.createFromTitle( title );
expect( store.find( todo ).id ).to.equal( todo.id );
expect( store.find( todo ).title ).to.equal( todo.title );
expect( store.find( todo ).completed ).to.equal( false );
todo.set( 'completed', true );
expect( store.find( todo ).id ).to.equal( todo.id );
expect( store.find( todo ).completed ).to.equal( true );
store.remove( todo );
});
});
/**
* Some acceptance testing for views
*/
describe( 'views/*', function() {
it( 'should validate clear button view', function( done ) {
require( [ 'text!app/views/clear_button.html' ], function( html ){
expect( html ).to.be.a( 'string' );
expect( html ).to.match( /completedCount/ );
expect( Em.Handlebars.compile( html ) ).to.not.throw( Error );
done();
});
});
it( 'should validate items view', function( done ) {
require( [ 'text!app/views/items.html' ], function( html ) {
expect( html ).to.be.a( 'string' );
expect( html ).to.match( /collection/ );
expect( html ).to.match( /id="todo-list"/ );
expect( html ).to.match( /Todos\.todosController/ );
expect( html ).to.match( /Checkbox/ );
expect( html ).to.match( /class="toggle"/ );
expect( Em.Handlebars.compile( html ) ).to.not.throw( Error );
done();
});
});
});
{{#collection id="todo-list" contentBinding="Todos.todosController" tagName="ul" itemClassBinding="content.completed content.editing" }}
{{#unless content.editing}}
{{#view Todos.todosController.todoView contentBinding="content" }}
{{view Ember.Checkbox valueBinding="content.completed" class="toggle"}}
{{#view Ember.View tagName="label" todoBinding="content"}}
{{todo.title}}
{{/view}}
{{view Ember.Button target="Todos.todosController" action="removeObject" class="destroy" todoBinding="content"}}
{{/view}}
{{else}}
{{view Todos.todosController.todoEditor todoBinding="content" valueBinding="content.title"}}
{{/unless}}
{{/collection}}
This diff is collapsed.
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
/*
RequireJS text 0.27.0 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved.
Available via the MIT or new BSD license.
see: http://github.com/jrburke/requirejs for details
*/
(function(){var k=["Msxml2.XMLHTTP","Microsoft.XMLHTTP","Msxml2.XMLHTTP.4.0"],n=/^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im,o=/<body[^>]*>\s*([\s\S]+)\s*<\/body>/im,i=typeof location!=="undefined"&&location.href,p=i&&location.protocol&&location.protocol.replace(/\:/,""),q=i&&location.hostname,r=i&&(location.port||void 0),j=[];define(function(){var g,h,l;typeof window!=="undefined"&&window.navigator&&window.document?h=function(a,b){var c=g.createXhr();c.open("GET",a,!0);c.onreadystatechange=
function(){c.readyState===4&&b(c.responseText)};c.send(null)}:typeof process!=="undefined"&&process.versions&&process.versions.node?(l=require.nodeRequire("fs"),h=function(a,b){b(l.readFileSync(a,"utf8"))}):typeof Packages!=="undefined"&&(h=function(a,b){var c=new java.io.File(a),e=java.lang.System.getProperty("line.separator"),c=new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(c),"utf-8")),d,f,g="";try{d=new java.lang.StringBuffer;(f=c.readLine())&&f.length()&&
f.charAt(0)===65279&&(f=f.substring(1));for(d.append(f);(f=c.readLine())!==null;)d.append(e),d.append(f);g=String(d.toString())}finally{c.close()}b(g)});return g={version:"0.27.0",strip:function(a){if(a){var a=a.replace(n,""),b=a.match(o);b&&(a=b[1])}else a="";return a},jsEscape:function(a){return a.replace(/(['\\])/g,"\\$1").replace(/[\f]/g,"\\f").replace(/[\b]/g,"\\b").replace(/[\n]/g,"\\n").replace(/[\t]/g,"\\t").replace(/[\r]/g,"\\r")},createXhr:function(){var a,b,c;if(typeof XMLHttpRequest!==
"undefined")return new XMLHttpRequest;else for(b=0;b<3;b++){c=k[b];try{a=new ActiveXObject(c)}catch(e){}if(a){k=[c];break}}if(!a)throw Error("createXhr(): XMLHttpRequest not available");return a},get:h,parseName:function(a){var b=!1,c=a.indexOf("."),e=a.substring(0,c),a=a.substring(c+1,a.length),c=a.indexOf("!");c!==-1&&(b=a.substring(c+1,a.length),b=b==="strip",a=a.substring(0,c));return{moduleName:e,ext:a,strip:b}},xdRegExp:/^((\w+)\:)?\/\/([^\/\\]+)/,useXhr:function(a,b,c,e){var d=g.xdRegExp.exec(a),
f;if(!d)return!0;a=d[2];d=d[3];d=d.split(":");f=d[1];d=d[0];return(!a||a===b)&&(!d||d===c)&&(!f&&!d||f===e)},finishLoad:function(a,b,c,e,d){c=b?g.strip(c):c;d.isBuild&&d.inlineText&&(j[a]=c);e(c)},load:function(a,b,c,e){var d=g.parseName(a),f=d.moduleName+"."+d.ext,m=b.toUrl(f),h=e&&e.text&&e.text.useXhr||g.useXhr;!i||h(m,p,q,r)?g.get(m,function(b){g.finishLoad(a,d.strip,b,c,e)}):b([f],function(a){g.finishLoad(d.moduleName+"."+d.ext,d.strip,a,c,e)})},write:function(a,b,c){if(b in j){var e=g.jsEscape(j[b]);
c.asModule(a+"!"+b,"define(function () { return '"+e+"';});\n")}},writeFile:function(a,b,c,e,d){var b=g.parseName(b),f=b.moduleName+"."+b.ext,h=c.toUrl(b.moduleName+"."+b.ext)+".js";g.load(f,c,function(){var b=function(a){return e(h,a)};b.asModule=function(a,b){return e.asModule(a,h,b)};g.write(a,f,b,d)},d)}}})})();
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