Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Roman Vishnevskii
d571dd2e56 Add inferno in todoapp benchmark 2017-04-24 17:10:12 +07:00
Roman Vishnevskii
02e7b40c07 Add todoapp benchmark 2017-04-24 16:00:32 +07:00
242 changed files with 71311 additions and 0 deletions

2
bench/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
elm-stuff
node_modules

27
bench/LICENCE Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2016, Evan Czaplicki
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

15
bench/elm-package.json Normal file
View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "4.0.5 <= v < 5.0.0",
"elm-lang/html": "1.1.0 <= v < 2.0.0"
},
"elm-version": "0.17.1 <= v < 0.18.0"
}

View file

@ -0,0 +1 @@
(function(){"use strict";angular.module("todoCtrl",[]).controller("TodoCtrl",function TodoCtrl($scope,$location){var TC=this;var todos=TC.todos=[];TC.ESCAPE_KEY=27;TC.editedTodo={};TC.updateRemainingCount=function(){TC.remainingCount=todos.filter(function(todo){return!todo.completed}).length;TC.allChecked=TC.remainingCount===0};function resetTodo(){TC.newTodo={title:"",completed:false}}resetTodo();if($location.path()===""){$location.path("/")}TC.location=$location;$scope.$watch("TC.location.path()",function(path){TC.statusFilter={"/active":{completed:false},"/completed":{completed:true}}[path]});TC.addTodo=function(){var newTitle=TC.newTodo.title=TC.newTodo.title.trim();if(newTitle.length===0){return}todos.push(TC.newTodo);resetTodo();TC.updateRemainingCount()};TC.editTodo=function(todo){TC.editedTodo=todo;TC.originalTodo=angular.copy(todo)};TC.doneEditing=function(todo,index){TC.editedTodo={};todo.title=todo.title.trim();if(!todo.title){TC.removeTodo(index)}TC.updateRemainingCount()};TC.revertEditing=function(index){TC.editedTodo={};todos[index]=TC.originalTodo;TC.updateRemainingCount()};TC.removeTodo=function(index){todos.splice(index,1);TC.updateRemainingCount()};TC.clearCompletedTodos=function(){TC.todos=todos=todos.filter(function(val){return!val.completed});TC.updateRemainingCount()};TC.markAll=function(completed){todos.forEach(function(todo){todo.completed=completed});TC.updateRemainingCount()}});angular.module("todoFocus",[]).directive("todoFocus",function($timeout){return function(scope,elem,attrs){scope.$watch(attrs.todoFocus,function(newVal){if(newVal){$timeout(function(){elem[0].focus()},0,false)}})}});angular.module("todomvc",["todoCtrl","todoFocus"])})();

View file

@ -0,0 +1,77 @@
<!doctype html>
<html lang="en" ng-app="todomvc" data-framework="angularjs">
<head>
<meta charset="utf-8">
<title>AngularJS • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<style>
[ng-cloak] {
display: none;
}
</style>
</head>
<body>
<section class="todoapp" ng-controller="TodoCtrl as TC">
<header class="header">
<h1>todos</h1>
<form id="todo-form">
<input class="new-todo" placeholder="What needs to be done?" ng-model="TC.newTodo.title" autofocus ng-keydown="($event.keyCode === 13) && TC.addTodo()">
</form>
</header>
<section class="main" ng-show="TC.todos.length" ng-cloak>
<input class="toggle-all" type="checkbox" ng-model="TC.allChecked" ng-click="TC.markAll(TC.allChecked)">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li ng-repeat="todo in TC.todos | filter:TC.statusFilter" ng-class="{completed: todo.completed, editing: todo === TC.editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="TC.updateAndStore()">
<label ng-dblclick="TC.editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="TC.removeTodo($index)"></button>
</div>
<form ng-submit="TC.doneEditing(todo, $index)">
<input class="edit" ng-trim="false" ng-model="todo.title" ng-blur="TC.doneEditing(todo, $index)" ng-keydown="($event.keyCode === TC.ESCAPE_KEY) && TC.revertEditing($index)" todo-focus="todo === TC.editedTodo">
</form>
</li>
</ul>
</section>
<footer class="footer" ng-show="TC.todos.length" ng-cloak>
<span class="todo-count"><strong>{{TC.remainingCount}}</strong>
<ng-pluralize count="TC.remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
</span>
<ul class="filters">
<li>
<a ng-class="{selected: TC.location.path() == '/'} " href="#/">All</a>
</li>
<li>
<a ng-class="{selected: TC.location.path() == '/active'}" href="#/active">Active</a>
</li>
<li>
<a ng-class="{selected: TC.location.path() == '/completed'}" href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed" ng-click="TC.clearCompletedTodos()" ng-show="TC.remainingCount < TC.todos.length">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Credits:
<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
<a href="http://ericbidelman.com">Eric Bidelman</a>,
<a href="http://jacobmumm.com">Jacob Mumm</a> and
<a href="http://blog.igorminar.com">Igor Minar</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script>
window.requestAnimationFrame = window.webkitRequestAnimationFrame = function(callback) {
callback();
};
</script>
<script src="node_modules/angular/angular.js"></script>
<script src="build.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,118 @@
/* jshint undef: true, unused: true */
/*global angular */
(function () {
'use strict';
angular.module('todoCtrl', [])
/**
* The main controller for the app. The controller:
* - retrieves and persists the model via the todoStorage service
* - exposes the model to the template and provides event handlers
*/
.controller('TodoCtrl', function TodoCtrl($scope, $location) {
var TC = this;
var todos = TC.todos = [];
TC.ESCAPE_KEY = 27;
TC.editedTodo = {};
TC.updateRemainingCount = function () {
TC.remainingCount = todos.filter(function (todo) { return !todo.completed; }).length;
TC.allChecked = (TC.remainingCount === 0);
}
function resetTodo() {
TC.newTodo = {title: '', completed: false};
}
resetTodo();
if ($location.path() === '') {
$location.path('/');
}
TC.location = $location;
$scope.$watch('TC.location.path()', function (path) {
TC.statusFilter = { '/active': {completed: false}, '/completed': {completed: true} }[path];
});
TC.addTodo = function () {
var newTitle = TC.newTodo.title = TC.newTodo.title.trim();
if (newTitle.length === 0) {
return;
}
todos.push(TC.newTodo);
resetTodo();
TC.updateRemainingCount();
};
TC.editTodo = function (todo) {
TC.editedTodo = todo;
// Clone the original todo to restore it on demand.
TC.originalTodo = angular.copy(todo);
};
TC.doneEditing = function (todo, index) {
TC.editedTodo = {};
todo.title = todo.title.trim();
if (!todo.title) {
TC.removeTodo(index);
}
TC.updateRemainingCount();
};
TC.revertEditing = function (index) {
TC.editedTodo = {};
todos[index] = TC.originalTodo;
TC.updateRemainingCount();
};
TC.removeTodo = function (index) {
todos.splice(index, 1);
TC.updateRemainingCount();
};
TC.clearCompletedTodos = function () {
TC.todos = todos = todos.filter(function (val) {
return !val.completed;
});
TC.updateRemainingCount();
};
TC.markAll = function (completed) {
todos.forEach(function (todo) {
todo.completed = completed;
});
TC.updateRemainingCount();
};
});
angular.module('todoFocus', [])
/**
* Directive that places focus on the element it is applied to when the expression it binds to evaluates to true
*/
.directive('todoFocus', function ($timeout) {
return function (scope, elem, attrs) {
scope.$watch(attrs.todoFocus, function (newVal) {
if (newVal) {
$timeout(function () {
elem[0].focus();
}, 0, false);
}
});
};
});
/**
* The main TodoMVC app module that pulls all dependency modules declared in same named files
*
* @type {angular.Module}
*/
angular.module('todomvc', ['todoCtrl', 'todoFocus']);
})();

View file

@ -0,0 +1,14 @@
{
"private": true,
"scripts": {
"make": "uglifyjs js/app.js -o build.min.js"
},
"dependencies": {
"angular": "^1.5.8",
"todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.2"
},
"devDependencies": {
"uglify-js": "^2.6.4"
}
}

View file

@ -0,0 +1,25 @@
# Angular TodoMVC for Benchmarking
This is an update of the
_[Angular TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/angularjs-perf)_
with the following changes:
* Upgrade to Angular 1.5.7
* Update todomvc-app-css to 2.0.6 to match other examples in this benchmark
* Remove localStorage functionality - not relevant to measuring render performance
* Concat JS into one file
* Minify with uglify
and additional performance optimizations:
#### Removed the deep $watch in favor of direct function calls
The original example uses $scope.$watch with object equality enabled to track changes to the array of todos and perform actions when changes are detected. This is not a good practice as it causes a deep array comparison to happen every digest cycle, which can significantly degrade performance when working with large collections. Instead we can perform the required actions directly whenever todos are modified, since we control all points of possible modification.
#### Removed "track by $index" from the ng-repeat directive
The original example uses the ng-repeat directive with a "track by $index" clause to display the list of todos. _[Refer to section "Tracking and Duplicates" here for information on using "track by".](https://code.angularjs.org/1.5.7/docs/api/ng/directive/ngRepeat)_ In this particular case it is not necessary to use it, as long as care is taken not to persist the "$$hashKey" property added by Angular to local storage, which would cause duplicates to appear when the collection is loaded and new items are added, triggering an exception. The version without the "track by" clause could reasonably be written by someone implementing this from scratch and demonstrates better performance.
# Building
1. `npm install`
2. `npm run make`
3. Open a local server (e.g. with `npm install -g http-server`) and open index.html

View file

@ -0,0 +1 @@
(function(){"use strict";angular.module("todoCtrl",[]).controller("TodoCtrl",function TodoCtrl($scope,$location){var TC=this;var todos=TC.todos=[];TC.ESCAPE_KEY=27;TC.editedTodo={};function resetTodo(){TC.newTodo={title:"",completed:false}}resetTodo();if($location.path()===""){$location.path("/")}TC.location=$location;$scope.$watch("TC.location.path()",function(path){TC.statusFilter={"/active":{completed:false},"/completed":{completed:true}}[path]});$scope.$watch("TC.todos",function(){TC.remainingCount=todos.filter(function(todo){return!todo.completed}).length;TC.allChecked=TC.remainingCount===0},true);TC.addTodo=function(){var newTitle=TC.newTodo.title=TC.newTodo.title.trim();if(newTitle.length===0){return}todos.push(TC.newTodo);resetTodo()};TC.editTodo=function(todo){TC.editedTodo=todo;TC.originalTodo=angular.copy(todo)};TC.doneEditing=function(todo,index){TC.editedTodo={};todo.title=todo.title.trim();if(!todo.title){TC.removeTodo(index)}};TC.revertEditing=function(index){TC.editedTodo={};todos[index]=TC.originalTodo};TC.removeTodo=function(index){todos.splice(index,1)};TC.clearCompletedTodos=function(){TC.todos=todos=todos.filter(function(val){return!val.completed})};TC.markAll=function(completed){todos.forEach(function(todo){todo.completed=completed})}});angular.module("todoFocus",[]).directive("todoFocus",function($timeout){return function(scope,elem,attrs){scope.$watch(attrs.todoFocus,function(newVal){if(newVal){$timeout(function(){elem[0].focus()},0,false)}})}});angular.module("todomvc",["todoCtrl","todoFocus"])})();

View file

@ -0,0 +1,77 @@
<!doctype html>
<html lang="en" ng-app="todomvc" data-framework="angularjs">
<head>
<meta charset="utf-8">
<title>AngularJS • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<style>
[ng-cloak] {
display: none;
}
</style>
</head>
<body>
<section class="todoapp" ng-controller="TodoCtrl as TC">
<header class="header">
<h1>todos</h1>
<form id="todo-form">
<input class="new-todo" placeholder="What needs to be done?" ng-model="TC.newTodo.title" autofocus ng-keydown="($event.keyCode === 13) && TC.addTodo()">
</form>
</header>
<section class="main" ng-show="TC.todos.length" ng-cloak>
<input class="toggle-all" type="checkbox" ng-model="TC.allChecked" ng-click="TC.markAll(TC.allChecked)">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li ng-repeat="todo in TC.todos | filter:TC.statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo === TC.editedTodo}">
<div class="view">
<input class="toggle" type="checkbox" ng-model="todo.completed">
<label ng-dblclick="TC.editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="TC.removeTodo($index)"></button>
</div>
<form ng-submit="TC.doneEditing(todo, $index)">
<input class="edit" ng-trim="false" ng-model="todo.title" ng-blur="TC.doneEditing(todo, $index)" ng-keydown="($event.keyCode === TC.ESCAPE_KEY) && TC.revertEditing($index)" todo-focus="todo === TC.editedTodo">
</form>
</li>
</ul>
</section>
<footer class="footer" ng-show="TC.todos.length" ng-cloak>
<span class="todo-count"><strong>{{TC.remainingCount}}</strong>
<ng-pluralize count="TC.remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
</span>
<ul class="filters">
<li>
<a ng-class="{selected: TC.location.path() == '/'} " href="#/">All</a>
</li>
<li>
<a ng-class="{selected: TC.location.path() == '/active'}" href="#/active">Active</a>
</li>
<li>
<a ng-class="{selected: TC.location.path() == '/completed'}" href="#/completed">Completed</a>
</li>
</ul>
<button class="clear-completed" ng-click="TC.clearCompletedTodos()" ng-show="TC.remainingCount < TC.todos.length">Clear completed</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Credits:
<a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
<a href="http://ericbidelman.com">Eric Bidelman</a>,
<a href="http://jacobmumm.com">Jacob Mumm</a> and
<a href="http://blog.igorminar.com">Igor Minar</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script>
window.requestAnimationFrame = window.webkitRequestAnimationFrame = function(callback) {
callback();
};
</script>
<script src="node_modules/angular/angular.js"></script>
<script src="build.min.js"></script>
</body>
</html>

View file

@ -0,0 +1,113 @@
/* jshint undef: true, unused: true */
/*global angular */
(function () {
'use strict';
angular.module('todoCtrl', [])
/**
* The main controller for the app. The controller:
* - retrieves and persists the model via the todoStorage service
* - exposes the model to the template and provides event handlers
*/
.controller('TodoCtrl', function TodoCtrl($scope, $location) {
var TC = this;
var todos = TC.todos = [];
TC.ESCAPE_KEY = 27;
TC.editedTodo = {};
function resetTodo() {
TC.newTodo = {title: '', completed: false};
}
resetTodo();
if ($location.path() === '') {
$location.path('/');
}
TC.location = $location;
$scope.$watch('TC.location.path()', function (path) {
TC.statusFilter = { '/active': {completed: false}, '/completed': {completed: true} }[path];
});
// 3rd argument `true` for deep object watching
$scope.$watch('TC.todos', function () {
TC.remainingCount = todos.filter(function (todo) { return !todo.completed; }).length;
TC.allChecked = (TC.remainingCount === 0);
}, true);
TC.addTodo = function () {
var newTitle = TC.newTodo.title = TC.newTodo.title.trim();
if (newTitle.length === 0) {
return;
}
todos.push(TC.newTodo);
resetTodo();
};
TC.editTodo = function (todo) {
TC.editedTodo = todo;
// Clone the original todo to restore it on demand.
TC.originalTodo = angular.copy(todo);
};
TC.doneEditing = function (todo, index) {
TC.editedTodo = {};
todo.title = todo.title.trim();
if (!todo.title) {
TC.removeTodo(index);
}
};
TC.revertEditing = function (index) {
TC.editedTodo = {};
todos[index] = TC.originalTodo;
};
TC.removeTodo = function (index) {
todos.splice(index, 1);
};
TC.clearCompletedTodos = function () {
TC.todos = todos = todos.filter(function (val) {
return !val.completed;
});
};
TC.markAll = function (completed) {
todos.forEach(function (todo) {
todo.completed = completed;
});
};
});
angular.module('todoFocus', [])
/**
* Directive that places focus on the element it is applied to when the expression it binds to evaluates to true
*/
.directive('todoFocus', function ($timeout) {
return function (scope, elem, attrs) {
scope.$watch(attrs.todoFocus, function (newVal) {
if (newVal) {
$timeout(function () {
elem[0].focus();
}, 0, false);
}
});
};
});
/**
* The main TodoMVC app module that pulls all dependency modules declared in same named files
*
* @type {angular.Module}
*/
angular.module('todomvc', ['todoCtrl', 'todoFocus']);
})();

View file

@ -0,0 +1,14 @@
{
"private": true,
"scripts": {
"make": "uglifyjs js/app.js -o build.min.js"
},
"dependencies": {
"angular": "^1.5.8",
"todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.2"
},
"devDependencies": {
"uglify-js": "^2.6.4"
}
}

View file

@ -0,0 +1,19 @@
# Angular TodoMVC for Benchmarking
This is an update of the
_[Angular TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/angularjs-perf)_
with the following changes:
* Upgrade to Angular 1.5.7
* Update todomvc-app-css to 2.0.6 to match other examples in this benchmark
* Remove localStorage functionality - not relevant to measuring render performance
* Concat JS into one file
* Minify with uglify
This example is listed as "performance optimized" in the TodoMVC repository, but doesn't appear to make any performance improvements to the basic example other than excluding all features not mentioned in the app specification. Therefore, it should count as having no optimizations for the purposes of this benchmark.
# Building
1. `npm install`
2. `npm run make`
3. Open a local server (e.g. with `npm install -g http-server`) and open index.html

View file

@ -0,0 +1,28 @@
node_modules/.bin
# ignore the source / documentation of each node module, but retain the
# distribution builds
node_modules/systemjs
!node_modules/systemjs/dist/system.src.js
node_modules/todomvc-app-css
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common
!node_modules/todomvc-common/base.css
node_modules/angular2
!node_modules/angular2/bundles/angular2-polyfills.js
!node_modules/angular2/bundles/angular2.dev.js
node_modules/rxjs
!node_modules/rxjs/bundles/Rx.js
# Ignore development dependencies
node_modules/tsd
node_modules/typescript
*.js
*.js.map
typings

View file

@ -0,0 +1,23 @@
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keydown.enter)="addTodo()">
</header>
<section class="main" *ngIf="todoStore.todos.length > 0">
<input class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">
<ul class="todo-list">
<li *ngFor="#todo of todoStore.todos; trackBy:identify" [class.completed]="todo.completed" [class.editing]="todo.editing">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed">
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" (click)="remove(todo)"></button>
</div>
<input class="edit" *ngIf="todo.editing" [value]="todo.title" #editedtodo (blur)="stopEditing(todo, editedtodo.value)" (keydown.enter)="updateEditingTodo(todo, editedtodo.value)" (keydown.escape)="cancelEditingTodo(todo)">
</li>
</ul>
</section>
<footer class="footer" *ngIf="todoStore.todos.length > 0">
<span class="todo-count"><strong>{{todoStore.getRemaining().length}}</strong> {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left</span>
<button class="clear-completed" *ngIf="todoStore.getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button>
</footer>
</section>

View file

@ -0,0 +1,62 @@
import {Component} from 'angular2/core';
import {TodoStore, Todo} from './services/store';
@Component({
selector: 'todo-app',
templateUrl: 'app/app.html'
})
export default class TodoApp {
todoStore: TodoStore;
newTodoText = '';
constructor(todoStore: TodoStore) {
this.todoStore = todoStore;
}
identify(index: number) {
return index;
}
stopEditing(todo: Todo, editedTitle: string) {
todo.title = editedTitle;
todo.editing = false;
}
cancelEditingTodo(todo: Todo) {
todo.editing = false;
}
updateEditingTodo(todo: Todo, editedTitle: string) {
editedTitle = editedTitle.trim();
todo.editing = false;
if (editedTitle.length === 0) {
return this.todoStore.remove(todo);
}
todo.title = editedTitle;
}
editTodo(todo: Todo) {
todo.editing = true;
}
removeCompleted() {
this.todoStore.removeCompleted();
}
toggleCompletion(todo: Todo) {
this.todoStore.toggleCompletion(todo);
}
remove(todo: Todo){
this.todoStore.remove(todo);
}
addTodo() {
if (this.newTodoText.trim().length) {
this.todoStore.add(this.newTodoText);
this.newTodoText = '';
}
}
}

View file

@ -0,0 +1,9 @@
///<reference path="../node_modules/angular2/typings/browser.d.ts"/>
import {bootstrap} from 'angular2/platform/browser';
import TodoApp from './app'
import {TodoStore} from './services/store';
import {enableProdMode} from 'angular2/core';
enableProdMode();
bootstrap(TodoApp, [TodoStore]);

View file

@ -0,0 +1,62 @@
export class Todo {
completed: Boolean;
editing: Boolean;
private _title: String;
get title() {
return this._title;
}
set title(value: String) {
this._title = value.trim();
}
constructor(title: String) {
this.completed = false;
this.editing = false;
this.title = title.trim();
}
}
export class TodoStore {
todos: Array<Todo>;
constructor() {
this.todos = [];
}
private getWithCompleted(completed: Boolean) {
return this.todos.filter((todo: Todo) => todo.completed === completed);
}
allCompleted() {
return this.todos.length === this.getCompleted().length;
}
setAllTo(completed: Boolean) {
this.todos.forEach((t: Todo) => t.completed = completed);
}
removeCompleted() {
this.todos = this.getWithCompleted(false);
}
getRemaining() {
return this.getWithCompleted(false);
}
getCompleted() {
return this.getWithCompleted(true);
}
toggleCompletion(todo: Todo) {
todo.completed = !todo.completed;
}
remove(todo: Todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
}
add(title: String) {
this.todos.push(new Todo(title));
}
}

View file

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en" data-framework="angular2">
<head>
<meta charset="utf-8">
<title>Angular2 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
</head>
<body>
<todo-app></todo-app>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>
Created by <a href="http://github.com/samccone">Sam Saccone</a> and <a href="http://github.com/colineberhardt">Colin Eberhardt</a>
using <a href="http://angular.io">Angular2</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script>
System.config({
packages: {
'app': {
format: 'cjs',
defaultExtension: 'js'
}
}
});
System.import('app/bootstrap');
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "tsc"
},
"devDependencies": {
"typescript": "^1.8.10"
},
"dependencies": {
"angular2": "^2.0.0-beta.17",
"es6-shim": "^0.35.1",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.6",
"systemjs": "0.19.6",
"todomvc-app-css": "^2.0.0",
"todomvc-common": "^1.0.1",
"zone.js": "^0.6.17"
}
}

View file

@ -0,0 +1,32 @@
# Angular 2 • [TodoMVC](http://todomvc.com)
> Angular is a development platform for creating applications using modern web standards. Angular includes a wealth of essential features such as mobile gestures, animations, filtering, routing, data binding, security, internationalization, and beautiful UI components. It's extremely modular, lightweight, and easy to learn.
## Resources
- [Website](https://angular.io/)
- [Documentation](https://angular.io/docs/ts/latest/)
### Articles
- [Angular 2 Beta Announcement](http://angularjs.blogspot.co.uk/2015/12/angular-2-beta.html)
### Support
- [StackOverflow](http://stackoverflow.com/questions/tagged/angular2)
- [Google Groups](https://groups.google.com/forum/#!forum/angular)
- [Twitter](http://twitter.com/angularjs)
- [Google+](https://plus.sandbox.google.com/+AngularJS/posts)
*Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.*
## Implementation
This app was built using TypeScript and Angular 2 beta. To make changes simply
* `npm i`
* `npm run dev`
## Credit
Created by [Sam Saccone](http://github.com/samccone)

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true
},
"files": [
"app/bootstrap.ts"
]
}

28
bench/implementations/angular-2/.gitignore vendored Executable file
View file

@ -0,0 +1,28 @@
node_modules/.bin
# ignore the source / documentation of each node module, but retain the
# distribution builds
node_modules/systemjs
!node_modules/systemjs/dist/system.src.js
node_modules/todomvc-app-css
!node_modules/todomvc-app-css/index.css
node_modules/todomvc-common
!node_modules/todomvc-common/base.css
node_modules/angular2
!node_modules/angular2/bundles/angular2-polyfills.js
!node_modules/angular2/bundles/angular2.dev.js
node_modules/rxjs
!node_modules/rxjs/bundles/Rx.js
# Ignore development dependencies
node_modules/tsd
node_modules/typescript
*.js
*.js.map
typings

View file

@ -0,0 +1,23 @@
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus="" [(ngModel)]="newTodoText" (keydown.enter)="addTodo()">
</header>
<section class="main" *ngIf="todoStore.todos.length > 0">
<input class="toggle-all" type="checkbox" *ngIf="todoStore.todos.length" #toggleall [checked]="todoStore.allCompleted()" (click)="todoStore.setAllTo(toggleall.checked)">
<ul class="todo-list">
<li *ngFor="#todo of todoStore.todos" [class.completed]="todo.completed" [class.editing]="todo.editing">
<div class="view">
<input class="toggle" type="checkbox" (click)="toggleCompletion(todo)" [checked]="todo.completed">
<label (dblclick)="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" (click)="remove(todo)"></button>
</div>
<input class="edit" *ngIf="todo.editing" [value]="todo.title" #editedtodo (blur)="stopEditing(todo, editedtodo.value)" (keydown.enter)="updateEditingTodo(todo, editedtodo.value)" (keydown.escape)="cancelEditingTodo(todo)">
</li>
</ul>
</section>
<footer class="footer" *ngIf="todoStore.todos.length > 0">
<span class="todo-count"><strong>{{todoStore.getRemaining().length}}</strong> {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left</span>
<button class="clear-completed" *ngIf="todoStore.getCompleted().length > 0" (click)="removeCompleted()">Clear completed</button>
</footer>
</section>

View file

@ -0,0 +1,58 @@
import {Component} from 'angular2/core';
import {TodoStore, Todo} from './services/store';
@Component({
selector: 'todo-app',
templateUrl: 'app/app.html'
})
export default class TodoApp {
todoStore: TodoStore;
newTodoText = '';
constructor(todoStore: TodoStore) {
this.todoStore = todoStore;
}
stopEditing(todo: Todo, editedTitle: string) {
todo.title = editedTitle;
todo.editing = false;
}
cancelEditingTodo(todo: Todo) {
todo.editing = false;
}
updateEditingTodo(todo: Todo, editedTitle: string) {
editedTitle = editedTitle.trim();
todo.editing = false;
if (editedTitle.length === 0) {
return this.todoStore.remove(todo);
}
todo.title = editedTitle;
}
editTodo(todo: Todo) {
todo.editing = true;
}
removeCompleted() {
this.todoStore.removeCompleted();
}
toggleCompletion(todo: Todo) {
this.todoStore.toggleCompletion(todo);
}
remove(todo: Todo){
this.todoStore.remove(todo);
}
addTodo() {
if (this.newTodoText.trim().length) {
this.todoStore.add(this.newTodoText);
this.newTodoText = '';
}
}
}

View file

@ -0,0 +1,9 @@
///<reference path="../node_modules/angular2/typings/browser.d.ts"/>
import {bootstrap} from 'angular2/platform/browser';
import TodoApp from './app'
import {TodoStore} from './services/store';
import {enableProdMode} from 'angular2/core';
enableProdMode();
bootstrap(TodoApp, [TodoStore]);

View file

@ -0,0 +1,62 @@
export class Todo {
completed: Boolean;
editing: Boolean;
private _title: String;
get title() {
return this._title;
}
set title(value: String) {
this._title = value.trim();
}
constructor(title: String) {
this.completed = false;
this.editing = false;
this.title = title.trim();
}
}
export class TodoStore {
todos: Array<Todo>;
constructor() {
this.todos = [];
}
private getWithCompleted(completed: Boolean) {
return this.todos.filter((todo: Todo) => todo.completed === completed);
}
allCompleted() {
return this.todos.length === this.getCompleted().length;
}
setAllTo(completed: Boolean) {
this.todos.forEach((t: Todo) => t.completed = completed);
}
removeCompleted() {
this.todos = this.getWithCompleted(false);
}
getRemaining() {
return this.getWithCompleted(false);
}
getCompleted() {
return this.getWithCompleted(true);
}
toggleCompletion(todo: Todo) {
todo.completed = !todo.completed;
}
remove(todo: Todo) {
this.todos.splice(this.todos.indexOf(todo), 1);
}
add(title: String) {
this.todos.push(new Todo(title));
}
}

View file

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en" data-framework="angular2">
<head>
<meta charset="utf-8">
<title>Angular2 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
</head>
<body>
<todo-app></todo-app>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>
Created by <a href="http://github.com/samccone">Sam Saccone</a> and <a href="http://github.com/colineberhardt">Colin Eberhardt</a>
using <a href="http://angular.io">Angular2</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script>
System.config({
packages: {
'app': {
format: 'cjs',
defaultExtension: 'js'
}
}
});
System.import('app/bootstrap');
</script>
</body>
</html>

View file

@ -0,0 +1,19 @@
{
"private": true,
"scripts": {
"dev": "tsc"
},
"devDependencies": {
"typescript": "^1.8.10"
},
"dependencies": {
"angular2": "^2.0.0-beta.17",
"es6-shim": "^0.35.1",
"reflect-metadata": "0.1.2",
"rxjs": "5.0.0-beta.6",
"systemjs": "0.19.6",
"todomvc-app-css": "^2.0.0",
"todomvc-common": "^1.0.1",
"zone.js": "^0.6.17"
}
}

View file

@ -0,0 +1,32 @@
# Angular 2 • [TodoMVC](http://todomvc.com)
> Angular is a development platform for creating applications using modern web standards. Angular includes a wealth of essential features such as mobile gestures, animations, filtering, routing, data binding, security, internationalization, and beautiful UI components. It's extremely modular, lightweight, and easy to learn.
## Resources
- [Website](https://angular.io/)
- [Documentation](https://angular.io/docs/ts/latest/)
### Articles
- [Angular 2 Beta Announcement](http://angularjs.blogspot.co.uk/2015/12/angular-2-beta.html)
### Support
- [StackOverflow](http://stackoverflow.com/questions/tagged/angular2)
- [Google Groups](https://groups.google.com/forum/#!forum/angular)
- [Twitter](http://twitter.com/angularjs)
- [Google+](https://plus.sandbox.google.com/+AngularJS/posts)
*Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.*
## Implementation
This app was built using TypeScript and Angular 2 beta. To make changes simply
* `npm i`
* `npm run dev`
## Credit
Created by [Sam Saccone](http://github.com/samccone)

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES5",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": true
},
"files": [
"app/bootstrap.ts"
]
}

View file

@ -0,0 +1,339 @@
module Todo where
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into four distinct parts:
1. Model - a full definition of the application's state
2. Update - a way to step the application state forward
3. View - a way to visualize our application state with HTML
4. Inputs - the signals necessary to manage events
This clean division of concerns is a core part of Elm. You can read more about
this in the Pong tutorial: http://elm-lang.org/blog/making-pong
This program is not particularly large, so definitely see the following
for notes on structuring more complex GUIs with Elm:
https://github.com/evancz/elm-architecture-tutorial/
-}
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Lazy exposing (lazy2, lazy3)
import Json.Decode as Json
import Signal exposing (Signal, Address)
import String
import Window
---- MODEL ----
-- The full application state of our todo app.
type alias Model =
{ tasks : List Task
, field : String
, uid : Int
, visibility : String
}
type alias Task =
{ description : String
, completed : Bool
, editing : Bool
, id : Int
}
newTask : String -> Int -> Task
newTask desc id =
{ description = desc
, completed = False
, editing = False
, id = id
}
emptyModel : Model
emptyModel =
{ tasks = []
, visibility = "All"
, field = ""
, uid = 0
}
---- UPDATE ----
-- A description of the kinds of actions that can be performed on the model of
-- our application. See the following for more info on this pattern and
-- some alternatives: https://github.com/evancz/elm-architecture-tutorial/
type Action
= NoOp
| UpdateField String
| EditingTask Int Bool
| UpdateTask Int String
| Add
| Delete Int
| DeleteComplete
| Check Int Bool
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on a given Action?
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
Add ->
{ model |
uid = model.uid + 1,
field = "",
tasks =
if String.isEmpty model.field
then model.tasks
else model.tasks ++ [newTask model.field model.uid]
}
UpdateField str ->
{ model | field = str }
EditingTask id isEditing ->
let updateTask t = if t.id == id then { t | editing = isEditing } else t
in
{ model | tasks = List.map updateTask model.tasks }
UpdateTask id task ->
let updateTask t = if t.id == id then { t | description = task } else t
in
{ model | tasks = List.map updateTask model.tasks }
Delete id ->
{ model | tasks = List.filter (\t -> t.id /= id) model.tasks }
DeleteComplete ->
{ model | tasks = List.filter (not << .completed) model.tasks }
Check id isCompleted ->
let updateTask t = if t.id == id then { t | completed = isCompleted } else t
in
{ model | tasks = List.map updateTask model.tasks }
CheckAll isCompleted ->
let updateTask t = { t | completed = isCompleted }
in
{ model | tasks = List.map updateTask model.tasks }
ChangeVisibility visibility ->
{ model | visibility = visibility }
---- VIEW ----
view : Address Action -> Model -> Html
view address model =
div
[ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ]
]
[ section
[ class "todoapp" ]
[ lazy2 taskEntry address model.field
, lazy3 taskList address model.visibility model.tasks
, lazy3 controls address model.visibility model.tasks
]
, infoFooter
]
onEnter : Address a -> a -> Attribute
onEnter address value =
on "keydown"
(Json.customDecoder keyCode is13)
(\_ -> Signal.message address value)
is13 : Int -> Result String ()
is13 code =
if code == 13 then Ok () else Err "not the right key code"
taskEntry : Address Action -> String -> Html
taskEntry address task =
header
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, on "input" targetValue (Signal.message address << UpdateField)
, onEnter address Add
]
[]
]
taskList : Address Action -> String -> List Task -> Html
taskList address visibility tasks =
let isVisible todo =
case visibility of
"Completed" -> todo.completed
"Active" -> not todo.completed
_ -> True
allCompleted = List.all .completed tasks
cssVisibility = if List.isEmpty tasks then "hidden" else "visible"
in
section
[ class "main"
, style [ ("visibility", cssVisibility) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick address (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul
[ class "todo-list" ]
(List.map (todoItem address) (List.filter isVisible tasks))
]
todoItem : Address Action -> Task -> Html
todoItem address todo =
li
[ classList [ ("completed", todo.completed), ("editing", todo.editing) ]
, key (toString todo.id)
]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked todo.completed
, onClick address (Check todo.id (not todo.completed))
]
[]
, label
[ onDoubleClick address (EditingTask todo.id True) ]
[ text todo.description ]
, button
[ class "destroy"
, onClick address (Delete todo.id)
]
[]
]
, input
[ class "edit"
, value todo.description
, name "title"
, id ("todo-" ++ toString todo.id)
, on "input" targetValue (Signal.message address << UpdateTask todo.id)
, onBlur address (EditingTask todo.id False)
, onEnter address (EditingTask todo.id False)
]
[]
]
controls : Address Action -> String -> List Task -> Html
controls address visibility tasks =
let tasksCompleted = List.length (List.filter .completed tasks)
tasksLeft = List.length tasks - tasksCompleted
item_ = if tasksLeft == 1 then " item" else " items"
in
footer
[ class "footer"
, hidden (List.isEmpty tasks)
]
[ span
[ class "todo-count" ]
[ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left")
]
, ul
[ class "filters" ]
[ visibilitySwap address "#/" "All" visibility
, text " "
, visibilitySwap address "#/active" "Active" visibility
, text " "
, visibilitySwap address "#/completed" "Completed" visibility
]
, button
[ class "clear-completed"
, hidden (tasksCompleted == 0)
, onClick address DeleteComplete
]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
]
visibilitySwap : Address Action -> String -> String -> String -> Html
visibilitySwap address uri visibility actualVisibility =
li
[ onClick address (ChangeVisibility visibility) ]
[ a [ href uri, classList [("selected", visibility == actualVisibility)] ] [ text visibility ] ]
infoFooter : Html
infoFooter =
footer [ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main =
Signal.map (view actions.address) model
-- manage the model of our application over time
model : Signal Model
model =
Signal.foldp update emptyModel actions.signal
-- actions from user input
actions : Signal.Mailbox Action
actions =
Signal.mailbox NoOp
port focus : Signal String
port focus =
let needsFocus act =
case act of
EditingTask id bool -> bool
_ -> False
toSelector act =
case act of
EditingTask id _ -> "#todo-" ++ toString id
_ -> ""
in
actions.signal
|> Signal.filter needsFocus (EditingTask 0 True)
|> Signal.map toSelector

View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"summary": "TodoMVC created with Elm and elm-html",
"repository": "https://github.com/evancz/elm-todomvc.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-html": "4.0.2 <= v < 5.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm • TodoMVC</title>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script type="text/javascript">
var todomvc = Elm.fullscreen(Elm.Todo);
todomvc.ports.focus.subscribe(function(selector) {
setTimeout(function() {
var nodes = document.querySelectorAll(selector);
if (nodes.length === 1 && document.activeElement !== nodes[0]) {
nodes[0].focus();
}
}, 50);
});
</script>
</html>

View file

@ -0,0 +1,382 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: 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);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View file

@ -0,0 +1,336 @@
module Todo where
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into four distinct parts:
1. Model - a full definition of the application's state
2. Update - a way to step the application state forward
3. View - a way to visualize our application state with HTML
4. Inputs - the signals necessary to manage events
This clean division of concerns is a core part of Elm. You can read more about
this in the Pong tutorial: http://elm-lang.org/blog/making-pong
This program is not particularly large, so definitely see the following
for notes on structuring more complex GUIs with Elm:
https://github.com/evancz/elm-architecture-tutorial/
-}
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as Json
import Signal exposing (Signal, Address)
import String
import Window
---- MODEL ----
-- The full application state of our todo app.
type alias Model =
{ tasks : List Task
, field : String
, uid : Int
, visibility : String
}
type alias Task =
{ description : String
, completed : Bool
, editing : Bool
, id : Int
}
newTask : String -> Int -> Task
newTask desc id =
{ description = desc
, completed = False
, editing = False
, id = id
}
emptyModel : Model
emptyModel =
{ tasks = []
, visibility = "All"
, field = ""
, uid = 0
}
---- UPDATE ----
-- A description of the kinds of actions that can be performed on the model of
-- our application. See the following for more info on this pattern and
-- some alternatives: https://github.com/evancz/elm-architecture-tutorial/
type Action
= NoOp
| UpdateField String
| EditingTask Int Bool
| UpdateTask Int String
| Add
| Delete Int
| DeleteComplete
| Check Int Bool
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on a given Action?
update : Action -> Model -> Model
update action model =
case action of
NoOp -> model
Add ->
{ model |
uid = model.uid + 1,
field = "",
tasks =
if String.isEmpty model.field
then model.tasks
else model.tasks ++ [newTask model.field model.uid]
}
UpdateField str ->
{ model | field = str }
EditingTask id isEditing ->
let updateTask t = if t.id == id then { t | editing = isEditing } else t
in
{ model | tasks = List.map updateTask model.tasks }
UpdateTask id task ->
let updateTask t = if t.id == id then { t | description = task } else t
in
{ model | tasks = List.map updateTask model.tasks }
Delete id ->
{ model | tasks = List.filter (\t -> t.id /= id) model.tasks }
DeleteComplete ->
{ model | tasks = List.filter (not << .completed) model.tasks }
Check id isCompleted ->
let updateTask t = if t.id == id then { t | completed = isCompleted } else t
in
{ model | tasks = List.map updateTask model.tasks }
CheckAll isCompleted ->
let updateTask t = { t | completed = isCompleted }
in
{ model | tasks = List.map updateTask model.tasks }
ChangeVisibility visibility ->
{ model | visibility = visibility }
---- VIEW ----
view : Address Action -> Model -> Html
view address model =
div
[ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ]
]
[ section
[ class "todoapp" ]
[ taskEntry address model.field
, taskList address model.visibility model.tasks
, controls address model.visibility model.tasks
]
, infoFooter
]
onEnter : Address a -> a -> Attribute
onEnter address value =
on "keydown"
(Json.customDecoder keyCode is13)
(\_ -> Signal.message address value)
is13 : Int -> Result String ()
is13 code =
if code == 13 then Ok () else Err "not the right key code"
taskEntry : Address Action -> String -> Html
taskEntry address task =
header
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, on "input" targetValue (Signal.message address << UpdateField)
, onEnter address Add
]
[]
]
taskList : Address Action -> String -> List Task -> Html
taskList address visibility tasks =
let isVisible todo =
case visibility of
"Completed" -> todo.completed
"Active" -> not todo.completed
_ -> True
allCompleted = List.all .completed tasks
cssVisibility = if List.isEmpty tasks then "hidden" else "visible"
in
section
[ class "main"
, style [ ("visibility", cssVisibility) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick address (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul
[ class "todo-list" ]
(List.map (todoItem address) (List.filter isVisible tasks))
]
todoItem : Address Action -> Task -> Html
todoItem address todo =
li
[ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked todo.completed
, onClick address (Check todo.id (not todo.completed))
]
[]
, label
[ onDoubleClick address (EditingTask todo.id True) ]
[ text todo.description ]
, button
[ class "destroy"
, onClick address (Delete todo.id)
]
[]
]
, input
[ class "edit"
, value todo.description
, name "title"
, id ("todo-" ++ toString todo.id)
, on "input" targetValue (Signal.message address << UpdateTask todo.id)
, onBlur address (EditingTask todo.id False)
, onEnter address (EditingTask todo.id False)
]
[]
]
controls : Address Action -> String -> List Task -> Html
controls address visibility tasks =
let tasksCompleted = List.length (List.filter .completed tasks)
tasksLeft = List.length tasks - tasksCompleted
item_ = if tasksLeft == 1 then " item" else " items"
in
footer
[ class "footer"
, hidden (List.isEmpty tasks)
]
[ span
[ class "todo-count" ]
[ strong [] [ text (toString tasksLeft) ]
, text (item_ ++ " left")
]
, ul
[ class "filters" ]
[ visibilitySwap address "#/" "All" visibility
, text " "
, visibilitySwap address "#/active" "Active" visibility
, text " "
, visibilitySwap address "#/completed" "Completed" visibility
]
, button
[ class "clear-completed"
, hidden (tasksCompleted == 0)
, onClick address DeleteComplete
]
[ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ]
]
visibilitySwap : Address Action -> String -> String -> String -> Html
visibilitySwap address uri visibility actualVisibility =
li
[ onClick address (ChangeVisibility visibility) ]
[ a [ href uri, classList [("selected", visibility == actualVisibility)] ] [ text visibility ] ]
infoFooter : Html
infoFooter =
footer [ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]
---- INPUTS ----
-- wire the entire application together
main : Signal Html
main =
Signal.map (view actions.address) model
-- manage the model of our application over time
model : Signal Model
model =
Signal.foldp update emptyModel actions.signal
-- actions from user input
actions : Signal.Mailbox Action
actions =
Signal.mailbox NoOp
port focus : Signal String
port focus =
let needsFocus act =
case act of
EditingTask id bool -> bool
_ -> False
toSelector act =
case act of
EditingTask id _ -> "#todo-" ++ toString id
_ -> ""
in
actions.signal
|> Signal.filter needsFocus (EditingTask 0 True)
|> Signal.map toSelector

View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"summary": "TodoMVC created with Elm and elm-html",
"repository": "https://github.com/evancz/elm-todomvc.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "3.0.0 <= v < 4.0.0",
"evancz/elm-html": "4.0.2 <= v < 5.0.0"
},
"elm-version": "0.16.0 <= v < 0.17.0"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,26 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm • TodoMVC</title>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script type="text/javascript">
var todomvc = Elm.fullscreen(Elm.Todo);
todomvc.ports.focus.subscribe(function(selector) {
setTimeout(function() {
var nodes = document.querySelectorAll(selector);
if (nodes.length === 1 && document.activeElement !== nodes[0]) {
nodes[0].focus();
}
}, 50);
});
</script>
</html>

View file

@ -0,0 +1,382 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: 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);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View file

@ -0,0 +1,381 @@
port module Todo exposing (main)
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into three key parts:
1. Model - a full definition of the application's state
2. Update - a way to step the application state forward
3. View - a way to visualize our application state with HTML
This clean division of concerns is a core part of Elm. You can read more about
this in <http://guide.elm-lang.org/architecture/index.html>
-}
import Html exposing (..)
import Html.App as App
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Keyed as Keyed
import Html.Lazy exposing (lazy, lazy2)
import Json.Decode as Json
import String
main : Program Never
main =
App.program
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
port focus : String -> Cmd msg
-- MODEL
-- The full application state of our todo app.
type alias Model =
{ entries : List Entry
, field : String
, uid : Int
, visibility : String
}
type alias Entry =
{ description : String
, completed : Bool
, editing : Bool
, id : Int
}
emptyModel : Model
emptyModel =
{ entries = []
, visibility = "All"
, field = ""
, uid = 0
}
newEntry : String -> Int -> Entry
newEntry desc id =
{ description = desc
, completed = False
, editing = False
, id = id
}
init : ( Model, Cmd Msg )
init =
emptyModel ! []
-- UPDATE
{-| Users of our app can trigger messages by clicking and typing. These
messages are fed into the `update` function as they occur, letting us react
to them.
-}
type Msg
= NoOp
| UpdateField String
| EditingEntry Int Bool
| UpdateEntry Int String
| Add
| Delete Int
| DeleteComplete
| Check Int Bool
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on a given Msg?
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
Add ->
{ model
| uid = model.uid + 1
, field = ""
, entries =
if String.isEmpty model.field then
model.entries
else
model.entries ++ [newEntry model.field model.uid]
}
! []
UpdateField str ->
{ model | field = str }
! []
EditingEntry id isEditing ->
let
updateEntry t =
if t.id == id then { t | editing = isEditing } else t
in
{ model | entries = List.map updateEntry model.entries }
! [ focus ("#todo-" ++ toString id) ]
UpdateEntry id task ->
let
updateEntry t =
if t.id == id then { t | description = task } else t
in
{ model | entries = List.map updateEntry model.entries }
! []
Delete id ->
{ model | entries = List.filter (\t -> t.id /= id) model.entries }
! []
DeleteComplete ->
{ model | entries = List.filter (not << .completed) model.entries }
! []
Check id isCompleted ->
let
updateEntry t =
if t.id == id then { t | completed = isCompleted } else t
in
{ model | entries = List.map updateEntry model.entries }
! []
CheckAll isCompleted ->
let
updateEntry t =
{ t | completed = isCompleted }
in
{ model | entries = List.map updateEntry model.entries }
! []
ChangeVisibility visibility ->
{ model | visibility = visibility }
! []
-- VIEW
view : Model -> Html Msg
view model =
div
[ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ]
]
[ section
[ class "todoapp" ]
[ lazy viewInput model.field
, lazy2 viewEntries model.visibility model.entries
, lazy2 viewControls model.visibility model.entries
]
, infoFooter
]
viewInput : String -> Html Msg
viewInput task =
header
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, onInput UpdateField
, onEnter Add
]
[]
]
onEnter : Msg -> Attribute Msg
onEnter msg =
let
tagger code =
if code == 13 then msg else NoOp
in
on "keydown" (Json.map tagger keyCode)
-- VIEW ALL ENTRIES
viewEntries : String -> List Entry -> Html Msg
viewEntries visibility entries =
let
isVisible todo =
case visibility of
"Completed" -> todo.completed
"Active" -> not todo.completed
_ -> True
allCompleted =
List.all .completed entries
cssVisibility =
if List.isEmpty entries then "hidden" else "visible"
in
section
[ class "main"
, style [ ("visibility", cssVisibility) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, Keyed.ul [ class "todo-list" ] <|
List.map viewKeyedEntry (List.filter isVisible entries)
]
-- VIEW INDIVIDUAL ENTRIES
viewKeyedEntry : Entry -> (String, Html Msg)
viewKeyedEntry todo =
( toString todo.id, lazy viewEntry todo )
viewEntry : Entry -> Html Msg
viewEntry todo =
li
[ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked todo.completed
, onClick (Check todo.id (not todo.completed))
]
[]
, label
[ onDoubleClick (EditingEntry todo.id True) ]
[ text todo.description ]
, button
[ class "destroy"
, onClick (Delete todo.id)
]
[]
]
, input
[ class "edit"
, value todo.description
, name "title"
, id ("todo-" ++ toString todo.id)
, onInput (UpdateEntry todo.id)
, onBlur (EditingEntry todo.id False)
, onEnter (EditingEntry todo.id False)
]
[]
]
-- VIEW CONTROLS AND FOOTER
viewControls : String -> List Entry -> Html Msg
viewControls visibility entries =
let
entriesCompleted =
List.length (List.filter .completed entries)
entriesLeft =
List.length entries - entriesCompleted
in
footer
[ class "footer"
, hidden (List.isEmpty entries)
]
[ lazy viewControlsCount entriesLeft
, lazy viewControlsFilters visibility
, lazy viewControlsClear entriesCompleted
]
viewControlsCount : Int -> Html Msg
viewControlsCount entriesLeft =
let
item_ =
if entriesLeft == 1 then " item" else " items"
in
span
[ class "todo-count" ]
[ strong [] [ text (toString entriesLeft) ]
, text (item_ ++ " left")
]
viewControlsFilters : String -> Html Msg
viewControlsFilters visibility =
ul
[ class "filters" ]
[ visibilitySwap "#/" "All" visibility
, text " "
, visibilitySwap "#/active" "Active" visibility
, text " "
, visibilitySwap "#/completed" "Completed" visibility
]
visibilitySwap : String -> String -> String -> Html Msg
visibilitySwap uri visibility actualVisibility =
li
[ onClick (ChangeVisibility visibility) ]
[ a [ href uri, classList [("selected", visibility == actualVisibility)] ]
[ text visibility ]
]
viewControlsClear : Int -> Html Msg
viewControlsClear entriesCompleted =
button
[ class "clear-completed"
, hidden (entriesCompleted == 0)
, onClick DeleteComplete
]
[ text ("Clear completed (" ++ toString entriesCompleted ++ ")")
]
infoFooter : Html msg
infoFooter =
footer [ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]

View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"summary": "TodoMVC created with Elm and elm-html",
"repository": "https://github.com/evancz/elm-todomvc.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.17.0 <= v < 0.18.0"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm • TodoMVC</title>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script type="text/javascript">
var todomvc = Elm.Todo.fullscreen();
todomvc.ports.focus.subscribe(function(selector)
{
setTimeout(function()
{
var nodes = document.querySelectorAll(selector);
if (nodes.length === 1 && document.activeElement !== nodes[0])
{
nodes[0].focus();
}
}, 50);
});
</script>
</html>

View file

@ -0,0 +1,382 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: 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);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View file

@ -0,0 +1,374 @@
port module Todo exposing (main)
{-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering.
This application is broken up into three key parts:
1. Model - a full definition of the application's state
2. Update - a way to step the application state forward
3. View - a way to visualize our application state with HTML
This clean division of concerns is a core part of Elm. You can read more about
this in <http://guide.elm-lang.org/architecture/index.html>
-}
import Html exposing (..)
import Html.App as App
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as Json
import String
main : Program Never
main =
App.program
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
port focus : String -> Cmd msg
-- MODEL
-- The full application state of our todo app.
type alias Model =
{ entries : List Entry
, field : String
, uid : Int
, visibility : String
}
type alias Entry =
{ description : String
, completed : Bool
, editing : Bool
, id : Int
}
emptyModel : Model
emptyModel =
{ entries = []
, visibility = "All"
, field = ""
, uid = 0
}
newEntry : String -> Int -> Entry
newEntry desc id =
{ description = desc
, completed = False
, editing = False
, id = id
}
init : ( Model, Cmd Msg )
init =
emptyModel ! []
-- UPDATE
{-| Users of our app can trigger messages by clicking and typing. These
messages are fed into the `update` function as they occur, letting us react
to them.
-}
type Msg
= NoOp
| UpdateField String
| EditingEntry Int Bool
| UpdateEntry Int String
| Add
| Delete Int
| DeleteComplete
| Check Int Bool
| CheckAll Bool
| ChangeVisibility String
-- How we update our Model on a given Msg?
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
Add ->
{ model
| uid = model.uid + 1
, field = ""
, entries =
if String.isEmpty model.field then
model.entries
else
model.entries ++ [newEntry model.field model.uid]
}
! []
UpdateField str ->
{ model | field = str }
! []
EditingEntry id isEditing ->
let
updateEntry t =
if t.id == id then { t | editing = isEditing } else t
in
{ model | entries = List.map updateEntry model.entries }
! [ focus ("#todo-" ++ toString id) ]
UpdateEntry id task ->
let
updateEntry t =
if t.id == id then { t | description = task } else t
in
{ model | entries = List.map updateEntry model.entries }
! []
Delete id ->
{ model | entries = List.filter (\t -> t.id /= id) model.entries }
! []
DeleteComplete ->
{ model | entries = List.filter (not << .completed) model.entries }
! []
Check id isCompleted ->
let
updateEntry t =
if t.id == id then { t | completed = isCompleted } else t
in
{ model | entries = List.map updateEntry model.entries }
! []
CheckAll isCompleted ->
let
updateEntry t =
{ t | completed = isCompleted }
in
{ model | entries = List.map updateEntry model.entries }
! []
ChangeVisibility visibility ->
{ model | visibility = visibility }
! []
-- VIEW
view : Model -> Html Msg
view model =
div
[ class "todomvc-wrapper"
, style [ ("visibility", "hidden") ]
]
[ section
[ class "todoapp" ]
[ viewInput model.field
, viewEntries model.visibility model.entries
, viewControls model.visibility model.entries
]
, infoFooter
]
viewInput : String -> Html Msg
viewInput task =
header
[ class "header" ]
[ h1 [] [ text "todos" ]
, input
[ class "new-todo"
, placeholder "What needs to be done?"
, autofocus True
, value task
, name "newTodo"
, onInput UpdateField
, onEnter Add
]
[]
]
onEnter : Msg -> Attribute Msg
onEnter msg =
let
tagger code =
if code == 13 then msg else NoOp
in
on "keydown" (Json.map tagger keyCode)
-- VIEW ALL ENTRIES
viewEntries : String -> List Entry -> Html Msg
viewEntries visibility entries =
let
isVisible todo =
case visibility of
"Completed" -> todo.completed
"Active" -> not todo.completed
_ -> True
allCompleted =
List.all .completed entries
cssVisibility =
if List.isEmpty entries then "hidden" else "visible"
in
section
[ class "main"
, style [ ("visibility", cssVisibility) ]
]
[ input
[ class "toggle-all"
, type' "checkbox"
, name "toggle"
, checked allCompleted
, onClick (CheckAll (not allCompleted))
]
[]
, label
[ for "toggle-all" ]
[ text "Mark all as complete" ]
, ul [ class "todo-list" ] <|
List.map viewEntry (List.filter isVisible entries)
]
-- VIEW INDIVIDUAL ENTRIES
viewEntry : Entry -> Html Msg
viewEntry todo =
li
[ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked todo.completed
, onClick (Check todo.id (not todo.completed))
]
[]
, label
[ onDoubleClick (EditingEntry todo.id True) ]
[ text todo.description ]
, button
[ class "destroy"
, onClick (Delete todo.id)
]
[]
]
, input
[ class "edit"
, value todo.description
, name "title"
, id ("todo-" ++ toString todo.id)
, onInput (UpdateEntry todo.id)
, onBlur (EditingEntry todo.id False)
, onEnter (EditingEntry todo.id False)
]
[]
]
-- VIEW CONTROLS AND FOOTER
viewControls : String -> List Entry -> Html Msg
viewControls visibility entries =
let
entriesCompleted =
List.length (List.filter .completed entries)
entriesLeft =
List.length entries - entriesCompleted
in
footer
[ class "footer"
, hidden (List.isEmpty entries)
]
[ viewControlsCount entriesLeft
, viewControlsFilters visibility
, viewControlsClear entriesCompleted
]
viewControlsCount : Int -> Html Msg
viewControlsCount entriesLeft =
let
item_ =
if entriesLeft == 1 then " item" else " items"
in
span
[ class "todo-count" ]
[ strong [] [ text (toString entriesLeft) ]
, text (item_ ++ " left")
]
viewControlsFilters : String -> Html Msg
viewControlsFilters visibility =
ul
[ class "filters" ]
[ visibilitySwap "#/" "All" visibility
, text " "
, visibilitySwap "#/active" "Active" visibility
, text " "
, visibilitySwap "#/completed" "Completed" visibility
]
visibilitySwap : String -> String -> String -> Html Msg
visibilitySwap uri visibility actualVisibility =
li
[ onClick (ChangeVisibility visibility) ]
[ a [ href uri, classList [("selected", visibility == actualVisibility)] ]
[ text visibility ]
]
viewControlsClear : Int -> Html Msg
viewControlsClear entriesCompleted =
button
[ class "clear-completed"
, hidden (entriesCompleted == 0)
, onClick DeleteComplete
]
[ text ("Clear completed (" ++ toString entriesCompleted ++ ")")
]
infoFooter : Html msg
infoFooter =
footer [ class "info" ]
[ p [] [ text "Double-click to edit a todo" ]
, p []
[ text "Written by "
, a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ]
]
, p []
[ text "Part of "
, a [ href "http://todomvc.com" ] [ text "TodoMVC" ]
]
]

View file

@ -0,0 +1,15 @@
{
"version": "1.0.0",
"summary": "TodoMVC created with Elm and elm-html",
"repository": "https://github.com/evancz/elm-todomvc.git",
"license": "BSD3",
"source-directories": [
"."
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.17.0 <= v < 0.18.0"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Elm • TodoMVC</title>
<script type="text/javascript" src="elm.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
</body>
<script type="text/javascript">
var todomvc = Elm.Todo.fullscreen();
todomvc.ports.focus.subscribe(function(selector)
{
setTimeout(function()
{
var nodes = document.querySelectorAll(selector);
if (nodes.length === 1 && document.activeElement !== nodes[0])
{
nodes[0].focus();
}
}, 50);
});
</script>
</html>

View file

@ -0,0 +1,382 @@
html,
body {
margin: 0;
padding: 0;
}
.todomvc-wrapper {
visibility: visible !important;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: 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);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
.new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.todo-list li label {
white-space: pre-line;
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}

View file

@ -0,0 +1,16 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/tmp
# dependencies
/node_modules
/bower_components
# misc
/.sass-cache
/connect.lock
/coverage/*
/libpeerconnection.log
npm-debug.log
testem.log

View file

@ -0,0 +1,15 @@
# Ember.js TodoMVC for Benchmarking
This is an update of the
_[Ember.js TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/emberjs)_
with the following changes:
* Upgrade to Ember 2.6.2
* Configure router to support relative URLs
* Remove localStorage functionality - not relevant to measuring render performance
# Building
1. `npm install`
2. `npm run make`
3. `open dist/index.html`

View file

@ -0,0 +1,18 @@
import Ember from 'ember';
import Resolver from './resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
let App;
Ember.MODEL_FACTORY_INJECTIONS = true;
App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver
});
loadInitializers(App, config.modulePrefix);
export default App;

View file

@ -0,0 +1,48 @@
import Ember from 'ember';
export default Ember.Component.extend({
repo: Ember.inject.service(),
tagName: 'li',
editing: false,
classNameBindings: ['todo.completed', 'editing'],
actions: {
startEditing() {
this.get('onStartEdit')();
this.set('editing', true);
Ember.run.scheduleOnce('afterRender', this, 'focusInput');
},
doneEditing(todoTitle) {
if (!this.get('editing')) { return; }
if (Ember.isBlank(todoTitle)) {
this.send('removeTodo');
} else {
this.set('todo.title', todoTitle.trim());
this.set('editing', false);
this.get('onEndEdit')();
}
},
handleKeydown(e) {
if (e.keyCode === 13) {
e.target.blur();
} else if (e.keyCode === 27) {
this.set('editing', false);
}
},
toggleCompleted(e) {
let todo = this.get('todo');
Ember.set(todo, 'completed', e.target.checked);
},
removeTodo() {
this.get('repo').delete(this.get('todo'));
}
},
focusInput() {
this.element.querySelector('input.edit').focus();
}
});

View file

@ -0,0 +1,26 @@
import Ember from 'ember';
export default Ember.Component.extend({
repo: Ember.inject.service(),
tagName: 'section',
elementId: 'main',
canToggle: true,
allCompleted: Ember.computed('todos.@each.completed', function () {
return this.get('todos').isEvery('completed');
}),
actions: {
enableToggle() {
this.set('canToggle', true);
},
disableToggle() {
this.set('canToggle', false);
},
toggleAll() {
let allCompleted = this.get('allCompleted');
this.get('todos').forEach(todo => Ember.set(todo, 'completed', !allCompleted));
}
}
});

View file

@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Controller.extend({
todos: Ember.computed.filterBy('model', 'completed', false)
});

View file

@ -0,0 +1,19 @@
import Ember from 'ember';
export default Ember.Controller.extend({
repo: Ember.inject.service(),
remaining: Ember.computed.filterBy('model', 'completed', false),
completed: Ember.computed.filterBy('model', 'completed'),
actions: {
createTodo(e) {
if (e.keyCode === 13 && !Ember.isBlank(e.target.value)) {
this.get('repo').add({ title: e.target.value.trim(), completed: false });
e.target.value = '';
}
},
clearCompleted() {
this.get('model').removeObjects(this.get('completed'));
}
}
});

View file

@ -0,0 +1,5 @@
import Ember from 'ember';
export default Ember.Controller.extend({
todos: Ember.computed.filterBy('model', 'completed', true)
});

View file

@ -0,0 +1,7 @@
import Ember from 'ember';
export function gt([n1, n2]/*, hash*/) {
return n1 > n2;
}
export default Ember.Helper.helper(gt);

View file

@ -0,0 +1,8 @@
import Ember from 'ember';
import { pluralize } from 'ember-inflector';
export function pluralizeHelper([singular, count]/*, hash*/) {
return count === 1 ? singular : pluralize(singular);
}
export default Ember.Helper.helper(pluralizeHelper);

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Todomvc</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
<link rel="stylesheet" href="assets/vendor.css">
<link rel="stylesheet" href="assets/todomvc.css">
{{content-for "head-footer"}}
</head>
<body>
{{content-for "body"}}
<script src="assets/vendor.js"></script>
<script src="assets/todomvc.js"></script>
{{content-for "body-footer"}}
</body>
</html>

View file

@ -0,0 +1,3 @@
import Resolver from 'ember-resolver';
export default Resolver;

View file

@ -0,0 +1,13 @@
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function () {
this.route('active');
this.route('completed');
});
export default Router;

View file

@ -0,0 +1,8 @@
import Ember from 'ember';
export default Ember.Route.extend({
repo: Ember.inject.service(),
model() {
return this.get('repo').findAll();
}
});

View file

@ -0,0 +1,19 @@
import Ember from 'ember';
export default Ember.Service.extend({
lastId: 0,
data: null,
findAll() {
return this.get('data') || this.set('data', []);
},
add(attrs) {
let todo = Object.assign({ id: this.incrementProperty('lastId') }, attrs);
this.get('data').pushObject(todo);
return todo;
},
delete(todo) {
this.get('data').removeObject(todo);
}
});

View file

@ -0,0 +1 @@
{{todo-list todos=todos}}

View file

@ -0,0 +1,29 @@
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input type="text" id="new-todo" class="new-todo" onkeydown={{action 'createTodo'}} placeholder="What needs to be done?" autofocus>
</header>
{{outlet}}
{{#if (gt model.length 0)}}
<footer id="footer">
<span id="todo-count"><strong>{{remaining.length}}</strong> {{pluralize 'item' remaining.length}} left</span>
<ul id="filters">
<li>{{#link-to "index" activeClass="selected"}}All{{/link-to}}</li>
<li>{{#link-to "active" activeClass="selected"}}Active{{/link-to}}</li>
<li>{{#link-to "completed" activeClass="selected"}}Completed{{/link-to}}</li>
</ul>
{{#if completed.length}}
<button id="clear-completed" onclick={{action 'clearCompleted'}}>Clear 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/cibernox">Miguel Camba</a>,
<a href="http://github.com/addyosmani">Addy Osmani</a>
</p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>

View file

@ -0,0 +1 @@
{{todo-list todos=todos}}

View file

@ -0,0 +1,6 @@
<div class="view">
<input type="checkbox" class="toggle" checked={{todo.completed}} onchange={{action 'toggleCompleted'}}>
<label ondblclick={{action 'startEditing'}}>{{todo.title}}</label>
<button onclick={{action 'removeTodo'}} class="destroy"></button>
</div>
<input type="text" class="edit" value={{todo.title}} onblur={{action 'doneEditing' value='target.value'}} onkeydown={{action 'handleKeydown'}} autofocus>

View file

@ -0,0 +1,10 @@
{{#if todos.length}}
{{#if canToggle}}
<input type="checkbox" id="toggle-all" checked={{allCompleted}} onchange={{action 'toggleAll'}}>
{{/if}}
<ul id="todo-list" class="todo-list">
{{#each todos as |todo|}}
{{todo-item todo=todo onStartEdit=(action 'disableToggle') onEndEdit=(action 'enableToggle')}}
{{/each}}
</ul>
{{/if}}

View file

@ -0,0 +1,3 @@
{{#if model.length}}
{{todo-list todos=model}}
{{/if}}

View file

@ -0,0 +1,9 @@
{
"name": "todomvc",
"dependencies": {
"ember": "~2.6.2",
"ember-cli-shims": "0.1.1",
"ember-cli-test-loader": "0.2.2",
"ember-qunit-notifications": "0.1.0"
}
}

View file

@ -0,0 +1,47 @@
/* jshint node: true */
module.exports = function (environment) {
var ENV = {
modulePrefix: 'todomvc',
environment: environment,
baseURL: null,
locationType: 'none',
EmberENV: {
FEATURES: {
// Here you can enable experimental features on an ember canary build
// e.g. 'with-controller': true
}
},
APP: {
// Here you can pass flags/options to your application instance
// when it is created
}
};
// if (environment === 'development') {
// ENV.APP.LOG_RESOLVER = true;
// ENV.APP.LOG_ACTIVE_GENERATION = true;
// ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
// ENV.APP.LOG_VIEW_LOOKUPS = true;
// }
if (environment === 'test') {
// Testem prefers this...
ENV.baseURL = '/';
ENV.locationType = 'none';
// keep test console output quieter
ENV.APP.LOG_ACTIVE_GENERATION = false;
ENV.APP.LOG_VIEW_LOOKUPS = false;
ENV.APP.rootElement = '#ember-testing';
}
// if (environment === 'production') {
// }
return ENV;
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Todomvc</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="todomvc/config/environment" content="%7B%22modulePrefix%22%3A%22todomvc%22%2C%22environment%22%3A%22production%22%2C%22baseURL%22%3Anull%2C%22locationType%22%3A%22none%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%7D%2C%22APP%22%3A%7B%22name%22%3A%22todomvc%22%2C%22version%22%3A%220.0.0+9b29a440%22%7D%2C%22exportApplicationGlobal%22%3Afalse%7D" />
<link rel="stylesheet" href="assets/vendor-7b5c98520910afa58d74e05ec86cd873.css" integrity="sha256-bsagGHduhay9QPLUFpddcZFq7Kmr2ScM3VKnWhdX8oM= sha512-eNsGN2aLecWPvoqNVH8oXK8o/IJ7rO5ti0zgS8lF8LiwmKUHdEIuFduwcDL1VLAt2r+3YjgDzoSNYK6c57pJzw==" >
<link rel="stylesheet" href="assets/todomvc-d41d8cd98f00b204e9800998ecf8427e.css" integrity="sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= sha512-z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" >
</head>
<body>
<script src="assets/vendor-01de4edc8253c2828fa6f36d3b1ca849.js" integrity="sha256-lBRb2M2secp4ojcJ0xM1d9YkRX7hKTz8KeKZlAWyh6Y= sha512-MfYfzPGAnzELmWDdNiIq7OMBhv2gLn5rwfQ9QtIcxibX0kdu21LLbuEv6RV7eK75XS5WHxksS0R2Xhghfl767A==" ></script>
<script src="assets/todomvc-b88bd0267dade133c112b65a28b90916.js" integrity="sha256-KmQwQXUtlnv5yK0MUO7F9qBoMauvZRkmuFO7VuiGoso= sha512-gtby61qJDGevaGdmHBxy1zA40OietcAdgZbUWgEuWhLqnnbkdcEbmkcx1VtYCVrGO63n2ehSzqnl0uFOiSsCqg==" ></script>
</body>
</html>

View file

@ -0,0 +1,3 @@
# http://www.robotstxt.org
User-agent: *
Disallow:

View file

@ -0,0 +1,26 @@
/*jshint node:true*/
/* global require, module */
var EmberApp = require('ember-cli/lib/broccoli/ember-app');
module.exports = function (defaults) {
var app = new EmberApp(defaults, {
// Add options here
});
app.import('vendor/base.css');
app.import('vendor/index.css');
// Use `app.import` to add additional libraries to the generated
// output files.
//
// If you need to use different assets in different
// environments, specify an object as the first parameter. That
// object's keys should be the environment name and the values
// should be the asset to use in that environment.
//
// If the library that you are including contains AMD or ES6
// modules that you would like to import into your application
// please specify an object with the list of modules as keys
// along with the exports of each module as its value.
return app.toTree();
};

View file

@ -0,0 +1,44 @@
{
"name": "todomvc",
"version": "0.0.0",
"description": "Small description for todomvc goes here",
"private": true,
"directories": {
"doc": "doc",
"test": "tests"
},
"scripts": {
"build": "ember build",
"start": "ember server",
"test": "ember test",
"make": "bower install && ember build -prod"
},
"repository": "",
"engines": {
"node": ">= 0.10.0"
},
"author": "",
"license": "MIT",
"devDependencies": {
"bower": "*",
"broccoli-asset-rev": "^2.4.2",
"ember-ajax": "^2.0.1",
"ember-cli": "2.6.3",
"ember-cli-app-version": "^1.0.0",
"ember-cli-babel": "^5.1.6",
"ember-cli-dependency-checker": "^1.2.0",
"ember-cli-htmlbars": "^1.0.3",
"ember-cli-htmlbars-inline-precompile": "^0.3.1",
"ember-cli-inject-live-reload": "^1.4.0",
"ember-cli-jshint": "^1.0.0",
"ember-cli-qunit": "^1.4.0",
"ember-cli-release": "^0.2.9",
"ember-cli-sri": "^2.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-export-application-global": "^1.0.5",
"ember-inflector": "1.9.4",
"ember-load-initializers": "^0.5.1",
"ember-resolver": "^2.0.3",
"loader.js": "^4.0.1"
}
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<!-- Read this: www.adobe.com/devnet/articles/crossdomain_policy_file_spec.html -->
<!-- Most restrictive policy: -->
<site-control permitted-cross-domain-policies="none"/>
<!-- Least restrictive policy: -->
<!--
<site-control permitted-cross-domain-policies="all"/>
<allow-access-from domain="*" to-ports="*" secure="false"/>
<allow-http-request-headers-from domain="*" headers="*" secure="false"/>
-->
</cross-domain-policy>

View file

@ -0,0 +1,3 @@
# http://www.robotstxt.org
User-agent: *
Disallow:

View file

@ -0,0 +1,13 @@
/*jshint node:true*/
module.exports = {
framework: 'qunit',
test_page: 'tests/index.html?hidepassed',
disable_watching: true,
launch_in_ci: [
'PhantomJS'
],
launch_in_dev: [
'PhantomJS',
'Chrome'
]
};

View file

@ -0,0 +1,141 @@
hr {
margin: 20px 0;
border: 0;
border-top: 1px dashed #c5c5c5;
border-bottom: 1px dashed #f7f7f7;
}
.learn a {
font-weight: normal;
text-decoration: none;
color: #b83f45;
}
.learn a:hover {
text-decoration: underline;
color: #787e7e;
}
.learn h3,
.learn h4,
.learn h5 {
margin: 10px 0;
font-weight: 500;
line-height: 1.2;
color: #000;
}
.learn h3 {
font-size: 24px;
}
.learn h4 {
font-size: 18px;
}
.learn h5 {
margin-bottom: 0;
font-size: 14px;
}
.learn ul {
padding: 0;
margin: 0 0 30px 25px;
}
.learn li {
line-height: 20px;
}
.learn p {
font-size: 15px;
font-weight: 300;
line-height: 1.3;
margin-top: 0;
margin-bottom: 0;
}
#issue-count {
display: none;
}
.quote {
border: none;
margin: 20px 0 60px 0;
}
.quote p {
font-style: italic;
}
.quote p:before {
content: '“';
font-size: 50px;
opacity: .15;
position: absolute;
top: -20px;
left: 3px;
}
.quote p:after {
content: '”';
font-size: 50px;
opacity: .15;
position: absolute;
bottom: -42px;
right: 3px;
}
.quote footer {
position: absolute;
bottom: -40px;
right: 0;
}
.quote footer img {
border-radius: 3px;
}
.quote footer a {
margin-left: 5px;
vertical-align: middle;
}
.speech-bubble {
position: relative;
padding: 10px;
background: rgba(0, 0, 0, .04);
border-radius: 5px;
}
.speech-bubble:after {
content: '';
position: absolute;
top: 100%;
right: 30px;
border: 13px solid transparent;
border-top-color: rgba(0, 0, 0, .04);
}
.learn-bar > .learn {
position: absolute;
width: 272px;
top: 8px;
left: -300px;
padding: 10px;
border-radius: 5px;
background-color: rgba(255, 255, 255, .6);
transition-property: left;
transition-duration: 500ms;
}
@media (min-width: 899px) {
.learn-bar {
width: auto;
padding-left: 300px;
}
.learn-bar > .learn {
left: 8px;
}
}

View file

@ -0,0 +1,378 @@
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #f5f5f5;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
}
.hidden {
display: none;
}
#todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: inherit;
font-weight: 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);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
}
#main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -55px;
left: -12px;
width: 60px;
height: 34px;
text-align: center;
border: none; /* Mobile Safari */
}
#toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
#toggle-all:checked:before {
color: #737373;
}
#todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
#todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
#todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
#todo-list li label {
white-space: pre;
word-break: break-word;
padding: 15px 60px 15px 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
#todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
margin-bottom: 11px;
transition: color 0.2s ease-out;
}
#todo-list li .destroy:hover {
color: #af5b5e;
}
#todo-list li .destroy:after {
content: '×';
}
#todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
color: #777;
padding: 10px 15px;
height: 20px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 50px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
0 8px 0 -3px #f6f6f6,
0 9px 1px -3px rgba(0, 0, 0, 0.2),
0 16px 0 -6px #f6f6f6,
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#todo-count strong {
font-weight: 300;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
#filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
#clear-completed,
html #clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
position: relative;
}
#clear-completed:hover {
text-decoration: underline;
}
#info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
#info p {
line-height: 1;
}
#info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
#toggle-all,
#todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
height: 40px;
}
#toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
@media (max-width: 430px) {
#footer {
height: 50px;
}
#filters {
bottom: 10px;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en" data-framework="inferno">
<head>
<meta charset="utf-8">
<title>Inferno • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head>
<body>
<section id="app" class="todoapp"></section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/lukeed/">Luke Edwards</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="node_modules/todomvc-common/base.js"></script>
<script src="dist/bundle.js"></script>
</body>
</html>

View file

@ -0,0 +1,24 @@
{
"private": true,
"scripts": {
"dev": "webpack-dev-server",
"build": "webpack -p",
"test": "cd test && npm test"
},
"dependencies": {
"inferno": "beta32",
"inferno-component": "beta32",
"todomvc-app-css": "^2.0.0",
"todomvc-common": "^1.0.0"
},
"devDependencies": {
"babel-core": "^6.5.2",
"babel-loader": "^6.2.1",
"babel-plugin-inferno": "beta13",
"babel-preset-es2015": "^6.5.0",
"babel-preset-es2015-loose": "^7.0.0",
"babel-preset-stage-0": "^6.5.0",
"webpack": "^1.12.14",
"webpack-dev-server": "^1.14.1"
}
}

View file

@ -0,0 +1,49 @@
import Inferno from 'inferno';
import { states } from './share';
/**
* Stateless Header component
*/
export function Head({onEnter}) {
return (
<header className="header">
<h1>todos</h1>
<input className="new-todo" autofocus onkeydown={ onEnter }
autocomplete="off" placeholder="What needs to be done?"
/>
</header>
);
}
export const links = [
{hash: '#/', name: 'All'},
{hash: '#/active', name: 'Active'},
{hash: '#/completed', name: 'Completed'}
];
/**
* Stateless Footer component
*/
export function Foot({left, done, route, onClear}) {
return (
<footer className="footer">
<span className="todo-count">
<strong>{ left }</strong> { left > 1 ? 'items' : 'item' } left
</span>
<ul className="filters">
{
links.map(({hash, name}) => (
<li>
<a href={ hash } className={ name.toLowerCase() === route ? 'selected' : '' }>
{ name }
</a>
</li>
))
}
</ul>
{ done > 0 ? (
<button className="clear-completed" onClick={ onClear }>Clear completed</button>
) : null }
</footer>
);
}

View file

@ -0,0 +1,112 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
import { ENTER, filters, read } from './share';
import { Head, Foot } from './base';
import Model from './model';
import Item from './item';
const { render } = Inferno;
const model = new Model();
class App extends Component {
state = {
route: read(),
todos: model.get()
};
update = arr => this.setState({todos: arr});
componentWillMount = () => {
window.onhashchange = () => this.setState({route: read()});
};
add = e => {
if (e.which !== ENTER) return;
const val = e.target.value.trim();
if (!val) return;
e.target.value = '';
this.update(
model.add(val)
);
};
edit = (todo, val) => {
val = val.trim();
if (val.length) {
this.update(
model.put(todo, {title: val, editing: 0})
);
} else {
this.remove(todo);
}
};
focus = todo => this.update(
model.put(todo, {editing: 1})
);
blur = todo => this.update(
model.put(todo, {editing: 0})
);
remove = todo => this.update(
model.del(todo)
);
toggleOne = todo => this.update(
model.toggle(todo)
);
toggleAll = ev => this.update(
model.toggleAll(ev.target.checked)
);
clearCompleted = () => this.update(
model.clearCompleted()
);
render(_, {todos, route}) {
const num = todos.length;
const shown = todos.filter(filters[route]);
const numDone = todos.filter(filters.completed).length;
const numAct = num - numDone;
return (
<div>
<Head onEnter={ this.add } />
{ num ? (
<section className="main">
<input className="toggle-all" type="checkbox"
onClick={ this.toggleAll } checked={ numAct === 0 }
/>
<ul className="todo-list">
{
shown.map(t =>
<Item data={t}
onBlur={ () => this.blur(t) }
onFocus={ () => this.focus(t) }
doDelete={ () => this.remove(t) }
doSave={ val => this.edit(t, val) }
doToggle={ () => this.toggleOne(t) }
/>
)
}
</ul>
</section>
) : null }
{ (numAct || numDone) ? (
<Foot onClear={ this.clearCompleted }
left={numAct} done={numDone} route={route}
/>
) : null }
</div>
)
}
}
render(<App />, document.getElementById('app'));

View file

@ -0,0 +1,58 @@
import Inferno from 'inferno';
import Component from 'inferno-component';
import { ESCAPE, ENTER, isEqual } from './share';
export default class Item extends Component {
constructor({data, ...props}) {
super(props);
this.todo = data;
this.state = {text: data.title};
this.editor = null;
}
componentWillReceiveProps = ({data}) => this.setText(data.title);
shouldComponentUpdate = ({data}, {text}) => !(isEqual(data, this.todo) && text === this.state.text);
componentWillUpdate = ({data}) => (this.todo = data);
componentDidUpdate = () => this.editor.focus();
setText = text => this.setState({text});
render({doToggle, doDelete, doSave, onBlur, onFocus}, {text}) {
const {title, completed, editing} = this.todo;
const cls = [];
editing && cls.push('editing');
completed && cls.push('completed');
const handleKeydown = e => {
if (e.which === ESCAPE) return onBlur();
if (e.which === ENTER) return doSave(text);
};
// tmp fix
const handleBlur = () => doSave(text);
const handleInput = e => this.setText(e.target.value);
return (
<li className={ cls.join(' ') }>
<div className="view">
<input className="toggle" type="checkbox"
checked={ completed } onClick={ doToggle }
/>
<label ondblclick={ onFocus }>{ title }</label>
<button className="destroy" onClick={ doDelete }></button>
</div>
<input className="edit"
ref={el => { this.editor = el }}
value={ editing && text }
onblur={ handleBlur }
oninput={ handleInput }
onkeydown={ handleKeydown }
/>
</li>
);
}
}

View file

@ -0,0 +1,36 @@
import { assign, isEqual } from './share';
const STOR = window.localStorage;
const STOR_ID = 'todos-inferno';
export default class Model {
get = () => (this.data = JSON.parse(STOR.getItem(STOR_ID) || '[]'))
set = arr => {
this.data = arr || this.data || [];
STOR.setItem(STOR_ID, JSON.stringify(this.data));
return this.data;
}
add = str => this.set(
this.data.concat({title: str, completed: false})
)
put = (todo, obj) => this.set(
this.data.map(t => isEqual(t, todo) ? assign(todo, obj) : t)
)
del = todo => this.set(
this.data.filter(t => !isEqual(t, todo))
)
toggle = todo => this.put(todo, {completed: !todo.completed})
toggleAll = completed => this.set(
this.data.map(t => ({...t, completed}))
)
clearCompleted = () => this.set(
this.data.filter(t => !t.completed)
)
}

View file

@ -0,0 +1,58 @@
/**
* Shared funcs/values
*/
export const ENTER = 13;
export const ESCAPE = 27;
export const filters = {
all: t => true,
active: t => !t.completed,
completed: t => t.completed
}
/**
* Read the `location.hash` value
* @return {String}
*/
export function read() {
return location.hash.replace('#/', '') || 'all';
}
/**
* Modified `Object.assign` shim
* - always writes to new object
* @return {Object}
*/
export function assign() {
let src;
let tar = {};
for (let s = 0; s < arguments.length; s++) {
src = Object(arguments[s]);
for (const k in src) {
tar[k] = src[k];
}
}
return tar;
}
/**
* Are two Objects equal values?
* @param {Object} a
* @param {Object} b
* @return {Boolean}
*/
export function isEqual(a, b) {
// Create arrays of property names
const aProps = Object.getOwnPropertyNames(a);
const bProps = Object.getOwnPropertyNames(b);
if (aProps.length !== bProps.length) return false;
for (let i = 0; i < aProps.length; i++) {
const k = aProps[i];
if (a[k] !== b[k]) return false;
}
return true;
}

View file

@ -0,0 +1,2 @@
node_modules
npm-debug.log

View file

@ -0,0 +1,189 @@
# TodoMVC Browser Tests
The TodoMVC project has a great many implementations of exactly the same app using different MV* frameworks. Each app should be functionally identical. The goal of these tests is to provide a fully automated browser-based test that can be used to ensure that the specification is being followed by each and every TodoMVC app.
## Todo
- [ ] Complete the test implementation (27 out of 28 are now complete). The only test that I am struggling with is to test that the delete button becomes visible on hover.
- [ ] Make it work with PhantomJS. In practice, Phantom is only a little bit faster, but it should work. Currently there are a few Phantom specific failures.
- [ ] Allow testing of apps that require a server (see: https://github.com/tastejs/todomvc/pull/821/files#r9377070)
## Running the tests
These tests use Selenium 2 (WebDriver), via the JavaScript API (WebdriverJS). In order to run the tests, you will need to install the dependencies. npm must be version 2.0.0 or greater, so upgrade it first with `npm install -g npm` if `npm --version` outputs anything less than 2.0.0. Run the following command from within the `tests` folder:
```sh
$ npm install
```
If you haven't already run `npm install` in the root directory, execute `npm install` there as well.
You need to run a local server for the tests. Start the test server using:
```sh
$ gulp test-server
```
To run the tests for all TodoMVC implementations, run the following:
```sh
$ npm run test
```
In order to run tests for a single TodoMVC implementation, supply a framework argument as follows:
```sh
$ npm run test -- --framework=angularjs
```
N.B. Remember the extra -- to separate the script arguments from the npm arguments.
In order to run a specific test, use the mocha 'grep' function. For example:
```
$ npm run test -- --framework=jquery --grep 'should trim entered text'
TodoMVC - jquery
Editing
✓ should trim entered text (1115ms)
1 passing (3s)
```
### Specifying the browser
You can also specify the browser that will be used to execute the tests via the `---browser` argument. The tests default to using Chrome. For example, to run against phantomjs, use the following:
```sh
$ npm run test -- --browser=phantomjs
```
You must install phantomjs first of course!
Valid browser names can be found within webdriver via the `webdriver.Browser` enumeration.
## Reporting against known issues
The `knownIssues.js` file details the currently known issues with the TodoMVC implementations. You can run the tests and compare against these issues using the `mocha-known-issues-reporter`:
```sh
$ npm run test -- --reporter=mocha-known-issues-reporter
```
When run via grunt the suite supports exactly the same command line arguments.
An example output with the known issues reporter is shown below:
```
$ npm run test -- --reporter=mocha-known-issues-reporter --framework=jquery
...
Running "simplemocha:files" (simplemocha) task
(1 of 27) pass: TodoMVC - jquery, No Todos, should hide #main and #footer
[...]
(17 of 27) pass: TodoMVC - jquery, Editing, should remove the item if an empty text string was entered
(18 of 27) known issue: TodoMVC - jquery, Editing, should cancel edits on escape -- error: undefined
(19 of 27) pass: TodoMVC - jquery, Counter, should display the current number of todo items
(20 of 27) pass: TodoMVC - jquery, Clear completed button, should display the number of completed items
(21 of 27) pass: TodoMVC - jquery, Clear completed button, should remove completed items when clicked
(22 of 27) pass: TodoMVC - jquery, Clear completed button, should be hidden when there are no items that are completed
(23 of 27) pass: TodoMVC - jquery, Persistence, should persist its data
(24 of 27) known issue: TodoMVC - jquery, Routing, should allow me to display active items -- error: Cannot call method 'click' of undefined
(25 of 27) known issue: TodoMVC - jquery, Routing, should allow me to display completed items -- error: Cannot call method 'click' of undefined
(26 of 27) known issue: TodoMVC - jquery, Routing, should allow me to display all items -- error: Cannot call method 'click' of undefined
(27 of 27) known issue: TodoMVC - jquery, Routing, should highlight the currently applied filter -- error: Cannot call method 'getAttribute' of undefined
passed: 22/27
failed: 5/27
new issues: 0
resolved issues: 0
```
The reporter indicates the number of passes, failed, new and resolved issues. This makes it ideal for regression testing.
### Example output
A test run with the 'spec' reporter looks something like the following:
```
$ npm run test -- --framework=angularjs
angularjs
TodoMVC
No Todos
✓ should hide #main and #footer (201ms)
New Todo
✓ should allow me to add todo items (548ms)
✓ should clear text input field when an item is added (306ms)
✓ should trim text input (569ms)
✓ should show #main and #footer when items added (405ms)
Mark all as completed
✓ should allow me to mark all items as completed (1040ms)
✓ should allow me to clear the completion state of all items (1014ms)
✓ complete all checkbox should update state when items are completed (1413ms)
Item
✓ should allow me to mark items as complete (843ms)
✓ should allow me to un-mark items as complete (978ms)
✓ should allow me to edit an item (1155ms)
✓ should show the remove button on hover
Editing
✓ should hide other controls when editing (718ms)
✓ should save edits on enter (1093ms)
✓ should save edits on blur (1256ms)
✓ should trim entered text (1163ms)
✓ should remove the item if an empty text string was entered (1033ms)
✓ should cancel edits on escape (1115ms)
Counter
✓ should display the current number of todo items (462ms)
Clear completed button
✓ should display the number of completed items (873ms)
✓ should remove completed items when clicked (898ms)
✓ should be hidden when there are no items that are completed (893ms)
Persistence
✓ should persist its data (3832ms)
Routing
✓ should allow me to display active items (871ms)
✓ should allow me to display completed items (960ms)
✓ should allow me to display all items (1192ms)
✓ should highlight the currently applied filter (1095ms)
27 passing (1m)
```
## Speed mode
In order to keep each test case fully isolated, the browser is closed then re-opened in between each test. This does mean that the tests can take quite a long time to run. If you don't mind the risk of side-effects you can run the tests in speed mode by adding the `--speedMode` argument.
```sh
$ npm run test -- --speedMode
```
Before each test, all the todo items are checked as completed and the 'clear complete' button pressed. This makes the tests run in around half the time, but with the obvious risk that the tear-down code may fail.
## Lax mode
There are certain implementations (e.g. GWT and Dojo) where the constraints of the framework mean that it is not possible to match exactly the HTML specification for TodoMVC. In these cases, the tests can be run in a 'lax' mode where the XPath queries used to locate DOM elements are more general. For example, rather than looking for a checkbox `input` element with a class of `toggle`, in lax mode it simply looks for any `input` elements of type `checkbox`. To run the tests in lax mode, simply use the `--laxMode` argument:
```sh
$ npm run test -- --laxMode
```
## Test design
Very briefly, the tests are designed as follows:
- `page.js` - provides an abstraction layer for the HTML template. All the code required to access elements from the DOM is found within this file. The XPaths used to locate elements are based on the TodoMVC specification, using the required element classes / ids.
- `pageLaxMode.js` - extends the above in order to relax the XPath constraints.
- `testOperations.js` - provides common assertions and operations.
- `test.js` - Erm … the tests! These are written to closely match the TodoMVC spec.
- `allTest.js` - A simple file that locates all of the framework examples, and runs the tests for each.
**NOTE:** All of the WebdriverJS methods return promises and are executed asynchronously. However, you do not have to 'chain' them using `then`, they are instead automagically added to a queue, then executed. This means that if you add non-WebdriverJS operations (asserts, log messages) these will not be executed at the point you might expect. This is why `TestOperations.js` uses an explicit `then` each time it asserts.

View file

@ -0,0 +1,414 @@
'use strict';
var webdriver = require('selenium-webdriver');
var chrome = require('selenium-webdriver/chrome');
var test = require('selenium-webdriver/testing');
var Page = require('./page');
var PageLaxMode = require('./pageLaxMode');
var TestOperations = require('./testOperations');
var argv = require('optimist').default('laxMode', false).default('browser', 'chrome').argv;
function todoMVCTest(baseUrl, speedMode, laxMode, browserName) {
test.describe('TodoMVC: ', function () {
var TODO_ITEM_ONE = 'buy some cheese';
var TODO_ITEM_TWO = 'feed the cat';
var TODO_ITEM_THREE = 'book a doctors appointment';
var browser, testOps, page;
// a number of tests use this set of ToDo items.
function createStandardItems(done) {
page.enterItem(TODO_ITEM_ONE);
page.enterItem(TODO_ITEM_TWO);
return page.enterItem(TODO_ITEM_THREE)
.then(function () {
if (done instanceof Function) {
done();
}
});
}
function launchBrowser(done) {
var chromeOptions = new chrome.Options();
chromeOptions.addArguments('no-sandbox');
if (process.env.CHROME_PATH !== undefined) {
chromeOptions.setChromeBinaryPath(process.env.CHROME_PATH);
}
browser = new webdriver.Builder()
.withCapabilities({browserName: browserName})
.setChromeOptions(chromeOptions)
.build();
browser.get(baseUrl);
page = laxMode ? new PageLaxMode(browser) : new Page(browser);
testOps = new TestOperations(page);
return page.ensureAppIsVisibleAndLoaded()
.then(function () {
if (done instanceof Function) {
done();
}
});
}
function printCapturedLogs() {
var logs = browser.manage().logs();
return logs.get('browser')
.then(function (entries) {
if (entries && entries.length) {
console.log(entries);
}
});
}
function closeBrowser(done) {
return browser
.quit()
.then(function () {
if (done instanceof Function) { done(); }
});
}
if (speedMode) {
test.before(launchBrowser);
test.after(closeBrowser);
test.afterEach(function (done) {
return browser.executeScript('window.localStorage && localStorage.clear(); location.reload(true);')
.then(function () { done(); });
});
} else {
test.beforeEach(launchBrowser);
test.afterEach(function (done) {
printCapturedLogs()
.then(function () {
return closeBrowser(done);
});
});
}
test.describe('When page is initially opened', function () {
test.it('should focus on the todo input field', function (done) {
testOps.assertNewInputFocused()
.then(function () { done(); });
});
});
test.describe('No Todos', function () {
test.it('should hide #main and #footer', function (done) {
testOps.assertItemCount(0);
testOps.assertMainSectionVisibility(false);
testOps.assertFooterVisibility(false)
.then(function () { done(); });
});
});
test.describe('New Todo', function () {
test.it('should allow me to add todo items', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertItems([TODO_ITEM_ONE]);
page.enterItem(TODO_ITEM_TWO);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO])
.then(function () { done(); });
});
test.it('should clear text input field when an item is added', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertNewItemInputFieldText('')
.then(function () { done(); });
});
test.it('should append new items to the bottom of the list', function (done) {
createStandardItems();
testOps.assertItemCount(3);
testOps.assertItemText(0, TODO_ITEM_ONE);
testOps.assertItemText(1, TODO_ITEM_TWO);
testOps.assertItemText(2, TODO_ITEM_THREE)
.then(function () { done(); });
});
test.it('should trim text input', function (done) {
page.enterItem(' ' + TODO_ITEM_ONE + ' ');
testOps.assertItemText(0, TODO_ITEM_ONE)
.then(function () { done(); });
});
test.it('should show #main and #footer when items added', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertMainSectionVisibility(true);
testOps.assertFooterVisibility(true)
.then(function () { done(); });
});
});
test.describe('Mark all as completed', function () {
test.beforeEach(createStandardItems);
test.it('should allow me to mark all items as completed', function (done) {
page.clickMarkAllCompletedCheckBox();
testOps.assertItemCompletedStates([true, true, true])
.then(function () { done(); });
});
test.it('should correctly update the complete all checked state', function (done) {
// manually check all items
page.toggleItemAtIndex(0);
page.toggleItemAtIndex(1);
page.toggleItemAtIndex(2);
// ensure checkall is in the correct state
testOps.assertCompleteAllCheckedStatus(true)
.then(function () { done(); });
});
test.it('should allow me to clear the completion state of all items', function (done) {
page.clickMarkAllCompletedCheckBox();
page.clickMarkAllCompletedCheckBox();
testOps.assertItemCompletedStates([false, false, false])
.then(function () { done(); });
});
test.it('complete all checkbox should update state when items are completed / cleared', function (done) {
page.clickMarkAllCompletedCheckBox();
testOps.assertCompleteAllCheckedStatus(true);
// all items are complete, now mark one as not-complete
page.toggleItemAtIndex(0);
testOps.assertCompleteAllCheckedStatus(false);
// now mark as complete, so that once again all items are completed
page.toggleItemAtIndex(0);
testOps.assertCompleteAllCheckedStatus(true)
.then(function () { done(); });
});
});
test.describe('Item', function () {
test.it('should allow me to mark items as complete', function (done) {
page.enterItem(TODO_ITEM_ONE);
page.enterItem(TODO_ITEM_TWO);
page.toggleItemAtIndex(0);
testOps.assertItemCompletedStates([true, false]);
page.toggleItemAtIndex(1);
testOps.assertItemCompletedStates([true, true])
.then(function () { done(); });
});
test.it('should allow me to un-mark items as complete', function (done) {
page.enterItem(TODO_ITEM_ONE);
page.enterItem(TODO_ITEM_TWO);
page.toggleItemAtIndex(0);
testOps.assertItemCompletedStates([true, false]);
page.toggleItemAtIndex(0);
testOps.assertItemCompletedStates([false, false])
.then(function () { done(); });
});
});
test.describe('Editing', function () {
test.beforeEach(function (done) {
createStandardItems();
page.doubleClickItemAtIndex(1)
.then(function () { done(); });
});
test.it('should focus the input', function (done) {
testOps.assertItemInputFocused();
testOps.assertNewInputBlurred() // Unnecessary? The HTML spec dictates that only one element can be focused.
.then(function () { done(); });
});
test.it('should hide other controls when editing', function (done) {
testOps.assertItemToggleIsHidden(1);
testOps.assertItemLabelIsHidden(1)
.then(function () { done(); });
});
test.it('should save edits on enter', function (done) {
page.editItemAtIndex(1, 'buy some sausages' + webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should save edits on blur', function (done) {
page.editItemAtIndex(1, 'buy some sausages');
// click a toggle button so that the blur() event is fired
page.toggleItemAtIndex(0);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should trim entered text', function (done) {
page.editItemAtIndex(1, ' buy some sausages ' + webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, 'buy some sausages', TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should remove the item if an empty text string was entered', function (done) {
page.editItemAtIndex(1, webdriver.Key.ENTER);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should cancel edits on escape', function (done) {
page.editItemAtIndex(1, 'foo' + webdriver.Key.ESCAPE);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
.then(function () { done(); });
});
});
test.describe('Counter', function () {
test.it('should display the current number of todo items', function (done) {
page.enterItem(TODO_ITEM_ONE);
testOps.assertItemCountText('1 item left');
page.enterItem(TODO_ITEM_TWO);
testOps.assertItemCountText('2 items left')
.then(function () { done(); });
});
});
test.describe('Clear completed button', function () {
test.beforeEach(createStandardItems);
test.it('should display the correct text', function (done) {
page.toggleItemAtIndex(1);
testOps.assertClearCompleteButtonText('Clear completed')
.then(function () { done(); });
});
test.it('should remove completed items when clicked', function (done) {
page.toggleItemAtIndex(1);
page.clickClearCompleteButton();
testOps.assertItemCount(2);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should be hidden when there are no items that are completed', function (done) {
page.toggleItemAtIndex(1);
testOps.assertClearCompleteButtonVisibility(true);
page.clickClearCompleteButton();
testOps.assertClearCompleteButtonVisibility(false)
.then(function () { done(); });
});
});
test.describe('Persistence', function () {
test.it('should persist its data', function (done) {
function stateTest() {
testOps.assertItemCount(2);
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO]);
return testOps.assertItemCompletedStates([false, true]);
}
// set up state
page.enterItem(TODO_ITEM_ONE);
page.enterItem(TODO_ITEM_TWO);
page.toggleItemAtIndex(1);
stateTest();
// navigate away and back again
browser.get('about:blank');
browser.get(baseUrl);
// repeat the state test
stateTest()
.then(function () { done(); });
});
});
test.describe('Routing', function () {
test.beforeEach(createStandardItems);
test.it('should allow me to display active items', function (done) {
page.toggleItemAtIndex(1);
page.filterByActiveItems();
testOps.assertItems([TODO_ITEM_ONE, page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_THREE])
.then(function () { return done(); });
});
test.it('should respect the back button', function (done) {
page.toggleItemAtIndex(1);
page.filterByActiveItems();
page.filterByCompletedItems();
testOps.assertItems([page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_TWO]); // should show completed items
page.back(); // then active items
testOps.assertItems([TODO_ITEM_ONE, page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_THREE]);
page.back(); // then all items
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should allow me to display completed items', function (done) {
page.toggleItemAtIndex(1);
page.filterByCompletedItems();
testOps.assertItems([page.ITEM_HIDDEN_OR_REMOVED, TODO_ITEM_TWO]);
page.filterByAllItems() // TODO: why
.then(function () { done(); });
});
test.it('should allow me to display all items', function (done) {
page.toggleItemAtIndex(1);
// apply the other filters first, before returning to the 'all' state
page.filterByActiveItems();
page.filterByCompletedItems();
page.filterByAllItems();
testOps.assertItems([TODO_ITEM_ONE, TODO_ITEM_TWO, TODO_ITEM_THREE])
.then(function () { done(); });
});
test.it('should highlight the currently applied filter', function (done) {
// initially 'all' should be selected
testOps.assertFilterAtIndexIsSelected(0);
page.filterByActiveItems();
testOps.assertFilterAtIndexIsSelected(1);
page.filterByCompletedItems();
testOps.assertFilterAtIndexIsSelected(2)
.then(function () { done(); });
});
});
});
};
'use strict';
/**
* Init test
*/
todoMVCTest(
'http://localhost:8000/index.html',
argv.speedMode,
argv.laxMode,
argv.browser
);

Some files were not shown because too many files have changed in this diff Show more