├── .gitignore ├── LICENCE ├── graphs ├── chrome.png ├── firefox.png ├── opera.png └── safari.png ├── implementations ├── angular-1.5.8-optimized │ ├── build.min.js │ ├── index.html │ ├── js │ │ └── app.js │ ├── package.json │ └── readme.md ├── angular-1.5.8 │ ├── build.min.js │ ├── index.html │ ├── js │ │ └── app.js │ ├── package.json │ └── readme.md ├── angular-2-optimized │ ├── .gitignore │ ├── app │ │ ├── app.html │ │ ├── app.ts │ │ ├── bootstrap.ts │ │ └── services │ │ │ └── store.ts │ ├── index.html │ ├── package.json │ ├── readme.md │ └── tsconfig.json ├── angular-2 │ ├── .gitignore │ ├── app │ │ ├── app.html │ │ ├── app.ts │ │ ├── bootstrap.ts │ │ └── services │ │ │ └── store.ts │ ├── index.html │ ├── package.json │ ├── readme.md │ └── tsconfig.json ├── elm-0.16-optimized │ ├── Todo.elm │ ├── elm-package.json │ ├── elm.js │ ├── index.html │ └── style.css ├── elm-0.16 │ ├── Todo.elm │ ├── elm-package.json │ ├── elm.js │ ├── index.html │ └── style.css ├── elm-0.17-optimized │ ├── Todo.elm │ ├── elm-package.json │ ├── elm.js │ ├── index.html │ └── style.css ├── elm-0.17 │ ├── Todo.elm │ ├── elm-package.json │ ├── elm.js │ ├── index.html │ └── style.css ├── ember-2.6.3 │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ ├── todo-item.js │ │ │ └── todo-list.js │ │ ├── controllers │ │ │ ├── active.js │ │ │ ├── application.js │ │ │ └── completed.js │ │ ├── helpers │ │ │ ├── gt.js │ │ │ └── pluralize.js │ │ ├── index.html │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── application.js │ │ ├── services │ │ │ └── repo.js │ │ ├── styles │ │ │ └── app.css │ │ └── templates │ │ │ ├── active.hbs │ │ │ ├── application.hbs │ │ │ ├── completed.hbs │ │ │ ├── components │ │ │ ├── todo-item.hbs │ │ │ └── todo-list.hbs │ │ │ └── index.hbs │ ├── bower.json │ ├── config │ │ └── environment.js │ ├── ember-cli-build.js │ ├── package.json │ ├── public │ │ ├── crossdomain.xml │ │ └── robots.txt │ ├── testem.js │ └── vendor │ │ ├── base.css │ │ └── index.css ├── react-15.3.1-optimized │ ├── build.min.js │ ├── index.html │ ├── js │ │ └── app.jsx │ ├── license.md │ ├── package.json │ └── readme.md └── react-15.3.1 │ ├── build.min.js │ ├── index.html │ ├── js │ └── app.jsx │ ├── license.md │ ├── package.json │ └── readme.md ├── index.html ├── readme.md └── src ├── Picker.elm ├── add-complete-delete-batched.js ├── add-complete-delete.js ├── elm-package.json ├── runner.js └── theme.css /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Evan Czaplicki 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /graphs/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evancz/react-angular-ember-elm-performance-comparison/6633af5a172dc43bed3bb4ce01bbbd0e32127a75/graphs/chrome.png -------------------------------------------------------------------------------- /graphs/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evancz/react-angular-ember-elm-performance-comparison/6633af5a172dc43bed3bb4ce01bbbd0e32127a75/graphs/firefox.png -------------------------------------------------------------------------------- /graphs/opera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evancz/react-angular-ember-elm-performance-comparison/6633af5a172dc43bed3bb4ce01bbbd0e32127a75/graphs/opera.png -------------------------------------------------------------------------------- /graphs/safari.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evancz/react-angular-ember-elm-performance-comparison/6633af5a172dc43bed3bb4ce01bbbd0e32127a75/graphs/safari.png -------------------------------------------------------------------------------- /implementations/angular-1.5.8-optimized/build.min.js: -------------------------------------------------------------------------------- 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"])})(); -------------------------------------------------------------------------------- /implementations/angular-1.5.8-optimized/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AngularJS • TodoMVC 7 | 8 | 9 | 14 | 15 | 16 | 17 |
18 |
19 |

todos

20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 39 |
40 | 57 |
58 | 68 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8-optimized/js/app.js: -------------------------------------------------------------------------------- 1 | /* jshint undef: true, unused: true */ 2 | /*global angular */ 3 | (function () { 4 | 'use strict'; 5 | 6 | angular.module('todoCtrl', []) 7 | 8 | /** 9 | * The main controller for the app. The controller: 10 | * - retrieves and persists the model via the todoStorage service 11 | * - exposes the model to the template and provides event handlers 12 | */ 13 | .controller('TodoCtrl', function TodoCtrl($scope, $location) { 14 | var TC = this; 15 | var todos = TC.todos = []; 16 | 17 | TC.ESCAPE_KEY = 27; 18 | TC.editedTodo = {}; 19 | 20 | TC.updateRemainingCount = function () { 21 | TC.remainingCount = todos.filter(function (todo) { return !todo.completed; }).length; 22 | TC.allChecked = (TC.remainingCount === 0); 23 | } 24 | 25 | function resetTodo() { 26 | TC.newTodo = {title: '', completed: false}; 27 | } 28 | 29 | resetTodo(); 30 | 31 | if ($location.path() === '') { 32 | $location.path('/'); 33 | } 34 | 35 | TC.location = $location; 36 | 37 | $scope.$watch('TC.location.path()', function (path) { 38 | TC.statusFilter = { '/active': {completed: false}, '/completed': {completed: true} }[path]; 39 | }); 40 | 41 | TC.addTodo = function () { 42 | var newTitle = TC.newTodo.title = TC.newTodo.title.trim(); 43 | if (newTitle.length === 0) { 44 | return; 45 | } 46 | 47 | todos.push(TC.newTodo); 48 | resetTodo(); 49 | TC.updateRemainingCount(); 50 | }; 51 | 52 | TC.editTodo = function (todo) { 53 | TC.editedTodo = todo; 54 | 55 | // Clone the original todo to restore it on demand. 56 | TC.originalTodo = angular.copy(todo); 57 | }; 58 | 59 | TC.doneEditing = function (todo, index) { 60 | TC.editedTodo = {}; 61 | todo.title = todo.title.trim(); 62 | 63 | if (!todo.title) { 64 | TC.removeTodo(index); 65 | } 66 | TC.updateRemainingCount(); 67 | }; 68 | 69 | TC.revertEditing = function (index) { 70 | TC.editedTodo = {}; 71 | todos[index] = TC.originalTodo; 72 | TC.updateRemainingCount(); 73 | }; 74 | 75 | TC.removeTodo = function (index) { 76 | todos.splice(index, 1); 77 | TC.updateRemainingCount(); 78 | }; 79 | 80 | TC.clearCompletedTodos = function () { 81 | TC.todos = todos = todos.filter(function (val) { 82 | return !val.completed; 83 | }); 84 | TC.updateRemainingCount(); 85 | }; 86 | 87 | TC.markAll = function (completed) { 88 | todos.forEach(function (todo) { 89 | todo.completed = completed; 90 | }); 91 | TC.updateRemainingCount(); 92 | }; 93 | }); 94 | 95 | 96 | angular.module('todoFocus', []) 97 | 98 | /** 99 | * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true 100 | */ 101 | .directive('todoFocus', function ($timeout) { 102 | return function (scope, elem, attrs) { 103 | scope.$watch(attrs.todoFocus, function (newVal) { 104 | if (newVal) { 105 | $timeout(function () { 106 | elem[0].focus(); 107 | }, 0, false); 108 | } 109 | }); 110 | }; 111 | }); 112 | /** 113 | * The main TodoMVC app module that pulls all dependency modules declared in same named files 114 | * 115 | * @type {angular.Module} 116 | */ 117 | angular.module('todomvc', ['todoCtrl', 'todoFocus']); 118 | })(); 119 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8-optimized/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "make": "uglifyjs js/app.js -o build.min.js" 5 | }, 6 | "dependencies": { 7 | "angular": "^1.5.8", 8 | "todomvc-app-css": "^2.0.6", 9 | "todomvc-common": "^1.0.2" 10 | }, 11 | "devDependencies": { 12 | "uglify-js": "^2.6.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8-optimized/readme.md: -------------------------------------------------------------------------------- 1 | # Angular TodoMVC for Benchmarking 2 | 3 | This is an update of the 4 | _[Angular TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/angularjs-perf)_ 5 | with the following changes: 6 | 7 | * Upgrade to Angular 1.5.7 8 | * Update todomvc-app-css to 2.0.6 to match other examples in this benchmark 9 | * Remove localStorage functionality - not relevant to measuring render performance 10 | * Concat JS into one file 11 | * Minify with uglify 12 | 13 | and additional performance optimizations: 14 | 15 | #### Removed the deep $watch in favor of direct function calls 16 | 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. 17 | 18 | #### Removed "track by $index" from the ng-repeat directive 19 | 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. 20 | 21 | # Building 22 | 23 | 1. `npm install` 24 | 2. `npm run make` 25 | 3. Open a local server (e.g. with `npm install -g http-server`) and open index.html 26 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8/build.min.js: -------------------------------------------------------------------------------- 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"])})(); -------------------------------------------------------------------------------- /implementations/angular-1.5.8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AngularJS • TodoMVC 7 | 8 | 9 | 14 | 15 | 16 | 17 |
18 |
19 |

todos

20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | 39 |
40 | 57 |
58 | 68 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8/js/app.js: -------------------------------------------------------------------------------- 1 | /* jshint undef: true, unused: true */ 2 | /*global angular */ 3 | (function () { 4 | 'use strict'; 5 | 6 | 7 | angular.module('todoCtrl', []) 8 | 9 | /** 10 | * The main controller for the app. The controller: 11 | * - retrieves and persists the model via the todoStorage service 12 | * - exposes the model to the template and provides event handlers 13 | */ 14 | .controller('TodoCtrl', function TodoCtrl($scope, $location) { 15 | var TC = this; 16 | var todos = TC.todos = []; 17 | 18 | TC.ESCAPE_KEY = 27; 19 | TC.editedTodo = {}; 20 | 21 | function resetTodo() { 22 | TC.newTodo = {title: '', completed: false}; 23 | } 24 | 25 | resetTodo(); 26 | 27 | if ($location.path() === '') { 28 | $location.path('/'); 29 | } 30 | 31 | TC.location = $location; 32 | 33 | $scope.$watch('TC.location.path()', function (path) { 34 | TC.statusFilter = { '/active': {completed: false}, '/completed': {completed: true} }[path]; 35 | }); 36 | 37 | // 3rd argument `true` for deep object watching 38 | $scope.$watch('TC.todos', function () { 39 | TC.remainingCount = todos.filter(function (todo) { return !todo.completed; }).length; 40 | TC.allChecked = (TC.remainingCount === 0); 41 | }, true); 42 | 43 | TC.addTodo = function () { 44 | var newTitle = TC.newTodo.title = TC.newTodo.title.trim(); 45 | if (newTitle.length === 0) { 46 | return; 47 | } 48 | 49 | todos.push(TC.newTodo); 50 | resetTodo(); 51 | }; 52 | 53 | TC.editTodo = function (todo) { 54 | TC.editedTodo = todo; 55 | 56 | // Clone the original todo to restore it on demand. 57 | TC.originalTodo = angular.copy(todo); 58 | }; 59 | 60 | TC.doneEditing = function (todo, index) { 61 | TC.editedTodo = {}; 62 | todo.title = todo.title.trim(); 63 | 64 | if (!todo.title) { 65 | TC.removeTodo(index); 66 | } 67 | }; 68 | 69 | TC.revertEditing = function (index) { 70 | TC.editedTodo = {}; 71 | todos[index] = TC.originalTodo; 72 | }; 73 | 74 | TC.removeTodo = function (index) { 75 | todos.splice(index, 1); 76 | }; 77 | 78 | TC.clearCompletedTodos = function () { 79 | TC.todos = todos = todos.filter(function (val) { 80 | return !val.completed; 81 | }); 82 | }; 83 | 84 | TC.markAll = function (completed) { 85 | todos.forEach(function (todo) { 86 | todo.completed = completed; 87 | }); 88 | }; 89 | }); 90 | 91 | angular.module('todoFocus', []) 92 | 93 | /** 94 | * Directive that places focus on the element it is applied to when the expression it binds to evaluates to true 95 | */ 96 | .directive('todoFocus', function ($timeout) { 97 | return function (scope, elem, attrs) { 98 | scope.$watch(attrs.todoFocus, function (newVal) { 99 | if (newVal) { 100 | $timeout(function () { 101 | elem[0].focus(); 102 | }, 0, false); 103 | } 104 | }); 105 | }; 106 | }); 107 | /** 108 | * The main TodoMVC app module that pulls all dependency modules declared in same named files 109 | * 110 | * @type {angular.Module} 111 | */ 112 | angular.module('todomvc', ['todoCtrl', 'todoFocus']); 113 | })(); 114 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "make": "uglifyjs js/app.js -o build.min.js" 5 | }, 6 | "dependencies": { 7 | "angular": "^1.5.8", 8 | "todomvc-app-css": "^2.0.6", 9 | "todomvc-common": "^1.0.2" 10 | }, 11 | "devDependencies": { 12 | "uglify-js": "^2.6.4" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /implementations/angular-1.5.8/readme.md: -------------------------------------------------------------------------------- 1 | # Angular TodoMVC for Benchmarking 2 | 3 | This is an update of the 4 | _[Angular TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/angularjs-perf)_ 5 | with the following changes: 6 | 7 | * Upgrade to Angular 1.5.7 8 | * Update todomvc-app-css to 2.0.6 to match other examples in this benchmark 9 | * Remove localStorage functionality - not relevant to measuring render performance 10 | * Concat JS into one file 11 | * Minify with uglify 12 | 13 | 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. 14 | 15 | # Building 16 | 17 | 1. `npm install` 18 | 2. `npm run make` 19 | 3. Open a local server (e.g. with `npm install -g http-server`) and open index.html 20 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/.bin 2 | 3 | # ignore the source / documentation of each node module, but retain the 4 | # distribution builds 5 | 6 | node_modules/systemjs 7 | !node_modules/systemjs/dist/system.src.js 8 | 9 | node_modules/todomvc-app-css 10 | !node_modules/todomvc-app-css/index.css 11 | 12 | node_modules/todomvc-common 13 | !node_modules/todomvc-common/base.css 14 | 15 | node_modules/angular2 16 | !node_modules/angular2/bundles/angular2-polyfills.js 17 | !node_modules/angular2/bundles/angular2.dev.js 18 | 19 | node_modules/rxjs 20 | !node_modules/rxjs/bundles/Rx.js 21 | 22 | # Ignore development dependencies 23 | node_modules/tsd 24 | node_modules/typescript 25 | 26 | *.js 27 | *.js.map 28 | typings 29 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/app/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

todos

4 | 5 |
6 |
7 | 8 | 18 |
19 | 23 |
24 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/app/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {TodoStore, Todo} from './services/store'; 3 | 4 | @Component({ 5 | selector: 'todo-app', 6 | templateUrl: 'app/app.html' 7 | }) 8 | export default class TodoApp { 9 | todoStore: TodoStore; 10 | newTodoText = ''; 11 | 12 | constructor(todoStore: TodoStore) { 13 | this.todoStore = todoStore; 14 | } 15 | 16 | identify(index: number) { 17 | return index; 18 | } 19 | 20 | stopEditing(todo: Todo, editedTitle: string) { 21 | todo.title = editedTitle; 22 | todo.editing = false; 23 | } 24 | 25 | cancelEditingTodo(todo: Todo) { 26 | todo.editing = false; 27 | } 28 | 29 | updateEditingTodo(todo: Todo, editedTitle: string) { 30 | editedTitle = editedTitle.trim(); 31 | todo.editing = false; 32 | 33 | if (editedTitle.length === 0) { 34 | return this.todoStore.remove(todo); 35 | } 36 | 37 | todo.title = editedTitle; 38 | } 39 | 40 | editTodo(todo: Todo) { 41 | todo.editing = true; 42 | } 43 | 44 | removeCompleted() { 45 | this.todoStore.removeCompleted(); 46 | } 47 | 48 | toggleCompletion(todo: Todo) { 49 | this.todoStore.toggleCompletion(todo); 50 | } 51 | 52 | remove(todo: Todo){ 53 | this.todoStore.remove(todo); 54 | } 55 | 56 | addTodo() { 57 | if (this.newTodoText.trim().length) { 58 | this.todoStore.add(this.newTodoText); 59 | this.newTodoText = ''; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {bootstrap} from 'angular2/platform/browser'; 4 | import TodoApp from './app' 5 | import {TodoStore} from './services/store'; 6 | import {enableProdMode} from 'angular2/core'; 7 | 8 | enableProdMode(); 9 | bootstrap(TodoApp, [TodoStore]); 10 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/app/services/store.ts: -------------------------------------------------------------------------------- 1 | export class Todo { 2 | completed: Boolean; 3 | editing: Boolean; 4 | 5 | private _title: String; 6 | get title() { 7 | return this._title; 8 | } 9 | set title(value: String) { 10 | this._title = value.trim(); 11 | } 12 | 13 | constructor(title: String) { 14 | this.completed = false; 15 | this.editing = false; 16 | this.title = title.trim(); 17 | } 18 | } 19 | 20 | export class TodoStore { 21 | todos: Array; 22 | 23 | constructor() { 24 | this.todos = []; 25 | } 26 | 27 | private getWithCompleted(completed: Boolean) { 28 | return this.todos.filter((todo: Todo) => todo.completed === completed); 29 | } 30 | 31 | allCompleted() { 32 | return this.todos.length === this.getCompleted().length; 33 | } 34 | 35 | setAllTo(completed: Boolean) { 36 | this.todos.forEach((t: Todo) => t.completed = completed); 37 | } 38 | 39 | removeCompleted() { 40 | this.todos = this.getWithCompleted(false); 41 | } 42 | 43 | getRemaining() { 44 | return this.getWithCompleted(false); 45 | } 46 | 47 | getCompleted() { 48 | return this.getWithCompleted(true); 49 | } 50 | 51 | toggleCompletion(todo: Todo) { 52 | todo.completed = !todo.completed; 53 | } 54 | 55 | remove(todo: Todo) { 56 | this.todos.splice(this.todos.indexOf(todo), 1); 57 | } 58 | 59 | add(title: String) { 60 | this.todos.push(new Todo(title)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular2 • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "tsc" 5 | }, 6 | "devDependencies": { 7 | "typescript": "^1.8.10" 8 | }, 9 | "dependencies": { 10 | "angular2": "^2.0.0-beta.17", 11 | "es6-shim": "^0.35.1", 12 | "reflect-metadata": "0.1.2", 13 | "rxjs": "5.0.0-beta.6", 14 | "systemjs": "0.19.6", 15 | "todomvc-app-css": "^2.0.0", 16 | "todomvc-common": "^1.0.1", 17 | "zone.js": "^0.6.17" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/readme.md: -------------------------------------------------------------------------------- 1 | # Angular 2 • [TodoMVC](http://todomvc.com) 2 | 3 | > 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. 4 | 5 | ## Resources 6 | 7 | - [Website](https://angular.io/) 8 | - [Documentation](https://angular.io/docs/ts/latest/) 9 | 10 | ### Articles 11 | 12 | - [Angular 2 Beta Announcement](http://angularjs.blogspot.co.uk/2015/12/angular-2-beta.html) 13 | 14 | ### Support 15 | 16 | - [StackOverflow](http://stackoverflow.com/questions/tagged/angular2) 17 | - [Google Groups](https://groups.google.com/forum/#!forum/angular) 18 | - [Twitter](http://twitter.com/angularjs) 19 | - [Google+](https://plus.sandbox.google.com/+AngularJS/posts) 20 | 21 | *Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.* 22 | 23 | ## Implementation 24 | 25 | This app was built using TypeScript and Angular 2 beta. To make changes simply 26 | 27 | * `npm i` 28 | * `npm run dev` 29 | 30 | ## Credit 31 | 32 | Created by [Sam Saccone](http://github.com/samccone) 33 | -------------------------------------------------------------------------------- /implementations/angular-2-optimized/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": false, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true 11 | }, 12 | "files": [ 13 | "app/bootstrap.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /implementations/angular-2/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/.bin 2 | 3 | # ignore the source / documentation of each node module, but retain the 4 | # distribution builds 5 | 6 | node_modules/systemjs 7 | !node_modules/systemjs/dist/system.src.js 8 | 9 | node_modules/todomvc-app-css 10 | !node_modules/todomvc-app-css/index.css 11 | 12 | node_modules/todomvc-common 13 | !node_modules/todomvc-common/base.css 14 | 15 | node_modules/angular2 16 | !node_modules/angular2/bundles/angular2-polyfills.js 17 | !node_modules/angular2/bundles/angular2.dev.js 18 | 19 | node_modules/rxjs 20 | !node_modules/rxjs/bundles/Rx.js 21 | 22 | # Ignore development dependencies 23 | node_modules/tsd 24 | node_modules/typescript 25 | 26 | *.js 27 | *.js.map 28 | typings 29 | -------------------------------------------------------------------------------- /implementations/angular-2/app/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

todos

4 | 5 |
6 |
7 | 8 |
    9 |
  • 10 |
    11 | 12 | 13 | 14 |
    15 | 16 |
  • 17 |
18 |
19 |
20 | {{todoStore.getRemaining().length}} {{todoStore.getRemaining().length == 1 ? 'item' : 'items'}} left 21 | 22 |
23 |
24 | -------------------------------------------------------------------------------- /implementations/angular-2/app/app.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | import {TodoStore, Todo} from './services/store'; 3 | 4 | @Component({ 5 | selector: 'todo-app', 6 | templateUrl: 'app/app.html' 7 | }) 8 | export default class TodoApp { 9 | todoStore: TodoStore; 10 | newTodoText = ''; 11 | 12 | constructor(todoStore: TodoStore) { 13 | this.todoStore = todoStore; 14 | } 15 | 16 | stopEditing(todo: Todo, editedTitle: string) { 17 | todo.title = editedTitle; 18 | todo.editing = false; 19 | } 20 | 21 | cancelEditingTodo(todo: Todo) { 22 | todo.editing = false; 23 | } 24 | 25 | updateEditingTodo(todo: Todo, editedTitle: string) { 26 | editedTitle = editedTitle.trim(); 27 | todo.editing = false; 28 | 29 | if (editedTitle.length === 0) { 30 | return this.todoStore.remove(todo); 31 | } 32 | 33 | todo.title = editedTitle; 34 | } 35 | 36 | editTodo(todo: Todo) { 37 | todo.editing = true; 38 | } 39 | 40 | removeCompleted() { 41 | this.todoStore.removeCompleted(); 42 | } 43 | 44 | toggleCompletion(todo: Todo) { 45 | this.todoStore.toggleCompletion(todo); 46 | } 47 | 48 | remove(todo: Todo){ 49 | this.todoStore.remove(todo); 50 | } 51 | 52 | addTodo() { 53 | if (this.newTodoText.trim().length) { 54 | this.todoStore.add(this.newTodoText); 55 | this.newTodoText = ''; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /implementations/angular-2/app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {bootstrap} from 'angular2/platform/browser'; 4 | import TodoApp from './app' 5 | import {TodoStore} from './services/store'; 6 | import {enableProdMode} from 'angular2/core'; 7 | 8 | enableProdMode(); 9 | bootstrap(TodoApp, [TodoStore]); 10 | -------------------------------------------------------------------------------- /implementations/angular-2/app/services/store.ts: -------------------------------------------------------------------------------- 1 | export class Todo { 2 | completed: Boolean; 3 | editing: Boolean; 4 | 5 | private _title: String; 6 | get title() { 7 | return this._title; 8 | } 9 | set title(value: String) { 10 | this._title = value.trim(); 11 | } 12 | 13 | constructor(title: String) { 14 | this.completed = false; 15 | this.editing = false; 16 | this.title = title.trim(); 17 | } 18 | } 19 | 20 | export class TodoStore { 21 | todos: Array; 22 | 23 | constructor() { 24 | this.todos = []; 25 | } 26 | 27 | private getWithCompleted(completed: Boolean) { 28 | return this.todos.filter((todo: Todo) => todo.completed === completed); 29 | } 30 | 31 | allCompleted() { 32 | return this.todos.length === this.getCompleted().length; 33 | } 34 | 35 | setAllTo(completed: Boolean) { 36 | this.todos.forEach((t: Todo) => t.completed = completed); 37 | } 38 | 39 | removeCompleted() { 40 | this.todos = this.getWithCompleted(false); 41 | } 42 | 43 | getRemaining() { 44 | return this.getWithCompleted(false); 45 | } 46 | 47 | getCompleted() { 48 | return this.getWithCompleted(true); 49 | } 50 | 51 | toggleCompletion(todo: Todo) { 52 | todo.completed = !todo.completed; 53 | } 54 | 55 | remove(todo: Todo) { 56 | this.todos.splice(this.todos.indexOf(todo), 1); 57 | } 58 | 59 | add(title: String) { 60 | this.todos.push(new Todo(title)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /implementations/angular-2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular2 • TodoMVC 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 23 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /implementations/angular-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "tsc" 5 | }, 6 | "devDependencies": { 7 | "typescript": "^1.8.10" 8 | }, 9 | "dependencies": { 10 | "angular2": "^2.0.0-beta.17", 11 | "es6-shim": "^0.35.1", 12 | "reflect-metadata": "0.1.2", 13 | "rxjs": "5.0.0-beta.6", 14 | "systemjs": "0.19.6", 15 | "todomvc-app-css": "^2.0.0", 16 | "todomvc-common": "^1.0.1", 17 | "zone.js": "^0.6.17" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /implementations/angular-2/readme.md: -------------------------------------------------------------------------------- 1 | # Angular 2 • [TodoMVC](http://todomvc.com) 2 | 3 | > 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. 4 | 5 | ## Resources 6 | 7 | - [Website](https://angular.io/) 8 | - [Documentation](https://angular.io/docs/ts/latest/) 9 | 10 | ### Articles 11 | 12 | - [Angular 2 Beta Announcement](http://angularjs.blogspot.co.uk/2015/12/angular-2-beta.html) 13 | 14 | ### Support 15 | 16 | - [StackOverflow](http://stackoverflow.com/questions/tagged/angular2) 17 | - [Google Groups](https://groups.google.com/forum/#!forum/angular) 18 | - [Twitter](http://twitter.com/angularjs) 19 | - [Google+](https://plus.sandbox.google.com/+AngularJS/posts) 20 | 21 | *Let us [know](https://github.com/tastejs/todomvc/issues) if you discover anything worth sharing.* 22 | 23 | ## Implementation 24 | 25 | This app was built using TypeScript and Angular 2 beta. To make changes simply 26 | 27 | * `npm i` 28 | * `npm run dev` 29 | 30 | ## Credit 31 | 32 | Created by [Sam Saccone](http://github.com/samccone) 33 | -------------------------------------------------------------------------------- /implementations/angular-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "sourceMap": false, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": true 11 | }, 12 | "files": [ 13 | "app/bootstrap.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /implementations/elm-0.16-optimized/Todo.elm: -------------------------------------------------------------------------------- 1 | module Todo where 2 | {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering. 3 | 4 | This application is broken up into four distinct parts: 5 | 6 | 1. Model - a full definition of the application's state 7 | 2. Update - a way to step the application state forward 8 | 3. View - a way to visualize our application state with HTML 9 | 4. Inputs - the signals necessary to manage events 10 | 11 | This clean division of concerns is a core part of Elm. You can read more about 12 | this in the Pong tutorial: http://elm-lang.org/blog/making-pong 13 | 14 | This program is not particularly large, so definitely see the following 15 | for notes on structuring more complex GUIs with Elm: 16 | https://github.com/evancz/elm-architecture-tutorial/ 17 | -} 18 | 19 | import Html exposing (..) 20 | import Html.Attributes exposing (..) 21 | import Html.Events exposing (..) 22 | import Html.Lazy exposing (lazy2, lazy3) 23 | import Json.Decode as Json 24 | import Signal exposing (Signal, Address) 25 | import String 26 | import Window 27 | 28 | 29 | ---- MODEL ---- 30 | 31 | -- The full application state of our todo app. 32 | type alias Model = 33 | { tasks : List Task 34 | , field : String 35 | , uid : Int 36 | , visibility : String 37 | } 38 | 39 | 40 | type alias Task = 41 | { description : String 42 | , completed : Bool 43 | , editing : Bool 44 | , id : Int 45 | } 46 | 47 | 48 | newTask : String -> Int -> Task 49 | newTask desc id = 50 | { description = desc 51 | , completed = False 52 | , editing = False 53 | , id = id 54 | } 55 | 56 | 57 | emptyModel : Model 58 | emptyModel = 59 | { tasks = [] 60 | , visibility = "All" 61 | , field = "" 62 | , uid = 0 63 | } 64 | 65 | 66 | ---- UPDATE ---- 67 | 68 | -- A description of the kinds of actions that can be performed on the model of 69 | -- our application. See the following for more info on this pattern and 70 | -- some alternatives: https://github.com/evancz/elm-architecture-tutorial/ 71 | type Action 72 | = NoOp 73 | | UpdateField String 74 | | EditingTask Int Bool 75 | | UpdateTask Int String 76 | | Add 77 | | Delete Int 78 | | DeleteComplete 79 | | Check Int Bool 80 | | CheckAll Bool 81 | | ChangeVisibility String 82 | 83 | 84 | -- How we update our Model on a given Action? 85 | update : Action -> Model -> Model 86 | update action model = 87 | case action of 88 | NoOp -> model 89 | 90 | Add -> 91 | { model | 92 | uid = model.uid + 1, 93 | field = "", 94 | tasks = 95 | if String.isEmpty model.field 96 | then model.tasks 97 | else model.tasks ++ [newTask model.field model.uid] 98 | } 99 | 100 | UpdateField str -> 101 | { model | field = str } 102 | 103 | EditingTask id isEditing -> 104 | let updateTask t = if t.id == id then { t | editing = isEditing } else t 105 | in 106 | { model | tasks = List.map updateTask model.tasks } 107 | 108 | UpdateTask id task -> 109 | let updateTask t = if t.id == id then { t | description = task } else t 110 | in 111 | { model | tasks = List.map updateTask model.tasks } 112 | 113 | Delete id -> 114 | { model | tasks = List.filter (\t -> t.id /= id) model.tasks } 115 | 116 | DeleteComplete -> 117 | { model | tasks = List.filter (not << .completed) model.tasks } 118 | 119 | Check id isCompleted -> 120 | let updateTask t = if t.id == id then { t | completed = isCompleted } else t 121 | in 122 | { model | tasks = List.map updateTask model.tasks } 123 | 124 | CheckAll isCompleted -> 125 | let updateTask t = { t | completed = isCompleted } 126 | in 127 | { model | tasks = List.map updateTask model.tasks } 128 | 129 | ChangeVisibility visibility -> 130 | { model | visibility = visibility } 131 | 132 | 133 | ---- VIEW ---- 134 | 135 | view : Address Action -> Model -> Html 136 | view address model = 137 | div 138 | [ class "todomvc-wrapper" 139 | , style [ ("visibility", "hidden") ] 140 | ] 141 | [ section 142 | [ class "todoapp" ] 143 | [ lazy2 taskEntry address model.field 144 | , lazy3 taskList address model.visibility model.tasks 145 | , lazy3 controls address model.visibility model.tasks 146 | ] 147 | , infoFooter 148 | ] 149 | 150 | 151 | onEnter : Address a -> a -> Attribute 152 | onEnter address value = 153 | on "keydown" 154 | (Json.customDecoder keyCode is13) 155 | (\_ -> Signal.message address value) 156 | 157 | 158 | is13 : Int -> Result String () 159 | is13 code = 160 | if code == 13 then Ok () else Err "not the right key code" 161 | 162 | 163 | taskEntry : Address Action -> String -> Html 164 | taskEntry address task = 165 | header 166 | [ class "header" ] 167 | [ h1 [] [ text "todos" ] 168 | , input 169 | [ class "new-todo" 170 | , placeholder "What needs to be done?" 171 | , autofocus True 172 | , value task 173 | , name "newTodo" 174 | , on "input" targetValue (Signal.message address << UpdateField) 175 | , onEnter address Add 176 | ] 177 | [] 178 | ] 179 | 180 | 181 | taskList : Address Action -> String -> List Task -> Html 182 | taskList address visibility tasks = 183 | let isVisible todo = 184 | case visibility of 185 | "Completed" -> todo.completed 186 | "Active" -> not todo.completed 187 | _ -> True 188 | 189 | allCompleted = List.all .completed tasks 190 | 191 | cssVisibility = if List.isEmpty tasks then "hidden" else "visible" 192 | in 193 | section 194 | [ class "main" 195 | , style [ ("visibility", cssVisibility) ] 196 | ] 197 | [ input 198 | [ class "toggle-all" 199 | , type' "checkbox" 200 | , name "toggle" 201 | , checked allCompleted 202 | , onClick address (CheckAll (not allCompleted)) 203 | ] 204 | [] 205 | , label 206 | [ for "toggle-all" ] 207 | [ text "Mark all as complete" ] 208 | , ul 209 | [ class "todo-list" ] 210 | (List.map (todoItem address) (List.filter isVisible tasks)) 211 | ] 212 | 213 | 214 | todoItem : Address Action -> Task -> Html 215 | todoItem address todo = 216 | li 217 | [ classList [ ("completed", todo.completed), ("editing", todo.editing) ] 218 | , key (toString todo.id) 219 | ] 220 | [ div 221 | [ class "view" ] 222 | [ input 223 | [ class "toggle" 224 | , type' "checkbox" 225 | , checked todo.completed 226 | , onClick address (Check todo.id (not todo.completed)) 227 | ] 228 | [] 229 | , label 230 | [ onDoubleClick address (EditingTask todo.id True) ] 231 | [ text todo.description ] 232 | , button 233 | [ class "destroy" 234 | , onClick address (Delete todo.id) 235 | ] 236 | [] 237 | ] 238 | , input 239 | [ class "edit" 240 | , value todo.description 241 | , name "title" 242 | , id ("todo-" ++ toString todo.id) 243 | , on "input" targetValue (Signal.message address << UpdateTask todo.id) 244 | , onBlur address (EditingTask todo.id False) 245 | , onEnter address (EditingTask todo.id False) 246 | ] 247 | [] 248 | ] 249 | 250 | 251 | controls : Address Action -> String -> List Task -> Html 252 | controls address visibility tasks = 253 | let tasksCompleted = List.length (List.filter .completed tasks) 254 | tasksLeft = List.length tasks - tasksCompleted 255 | item_ = if tasksLeft == 1 then " item" else " items" 256 | in 257 | footer 258 | [ class "footer" 259 | , hidden (List.isEmpty tasks) 260 | ] 261 | [ span 262 | [ class "todo-count" ] 263 | [ strong [] [ text (toString tasksLeft) ] 264 | , text (item_ ++ " left") 265 | ] 266 | , ul 267 | [ class "filters" ] 268 | [ visibilitySwap address "#/" "All" visibility 269 | , text " " 270 | , visibilitySwap address "#/active" "Active" visibility 271 | , text " " 272 | , visibilitySwap address "#/completed" "Completed" visibility 273 | ] 274 | , button 275 | [ class "clear-completed" 276 | , hidden (tasksCompleted == 0) 277 | , onClick address DeleteComplete 278 | ] 279 | [ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ] 280 | ] 281 | 282 | 283 | visibilitySwap : Address Action -> String -> String -> String -> Html 284 | visibilitySwap address uri visibility actualVisibility = 285 | li 286 | [ onClick address (ChangeVisibility visibility) ] 287 | [ a [ href uri, classList [("selected", visibility == actualVisibility)] ] [ text visibility ] ] 288 | 289 | 290 | infoFooter : Html 291 | infoFooter = 292 | footer [ class "info" ] 293 | [ p [] [ text "Double-click to edit a todo" ] 294 | , p [] 295 | [ text "Written by " 296 | , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] 297 | ] 298 | , p [] 299 | [ text "Part of " 300 | , a [ href "http://todomvc.com" ] [ text "TodoMVC" ] 301 | ] 302 | ] 303 | 304 | 305 | ---- INPUTS ---- 306 | 307 | -- wire the entire application together 308 | main : Signal Html 309 | main = 310 | Signal.map (view actions.address) model 311 | 312 | 313 | -- manage the model of our application over time 314 | model : Signal Model 315 | model = 316 | Signal.foldp update emptyModel actions.signal 317 | 318 | 319 | -- actions from user input 320 | actions : Signal.Mailbox Action 321 | actions = 322 | Signal.mailbox NoOp 323 | 324 | 325 | port focus : Signal String 326 | port focus = 327 | let needsFocus act = 328 | case act of 329 | EditingTask id bool -> bool 330 | _ -> False 331 | 332 | toSelector act = 333 | case act of 334 | EditingTask id _ -> "#todo-" ++ toString id 335 | _ -> "" 336 | in 337 | actions.signal 338 | |> Signal.filter needsFocus (EditingTask 0 True) 339 | |> Signal.map toSelector 340 | -------------------------------------------------------------------------------- /implementations/elm-0.16-optimized/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "TodoMVC created with Elm and elm-html", 4 | "repository": "https://github.com/evancz/elm-todomvc.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 12 | "evancz/elm-html": "4.0.2 <= v < 5.0.0" 13 | }, 14 | "elm-version": "0.16.0 <= v < 0.17.0" 15 | } 16 | -------------------------------------------------------------------------------- /implementations/elm-0.16-optimized/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /implementations/elm-0.16-optimized/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '❯'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml;utf8,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: '×'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li .edit { 246 | display: none; 247 | } 248 | 249 | .todo-list li.editing:last-child { 250 | margin-bottom: -1px; 251 | } 252 | 253 | .footer { 254 | color: #777; 255 | padding: 10px 15px; 256 | height: 20px; 257 | text-align: center; 258 | border-top: 1px solid #e6e6e6; 259 | } 260 | 261 | .footer:before { 262 | content: ''; 263 | position: absolute; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | height: 50px; 268 | overflow: hidden; 269 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 270 | 0 8px 0 -3px #f6f6f6, 271 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 272 | 0 16px 0 -6px #f6f6f6, 273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | .todo-count { 277 | float: left; 278 | text-align: left; 279 | } 280 | 281 | .todo-count strong { 282 | font-weight: 300; 283 | } 284 | 285 | .filters { 286 | margin: 0; 287 | padding: 0; 288 | list-style: none; 289 | position: absolute; 290 | right: 0; 291 | left: 0; 292 | } 293 | 294 | .filters li { 295 | display: inline; 296 | } 297 | 298 | .filters li a { 299 | color: inherit; 300 | margin: 3px; 301 | padding: 3px 7px; 302 | text-decoration: none; 303 | border: 1px solid transparent; 304 | border-radius: 3px; 305 | } 306 | 307 | .filters li a.selected, 308 | .filters li a:hover { 309 | border-color: rgba(175, 47, 47, 0.1); 310 | } 311 | 312 | .filters li a.selected { 313 | border-color: rgba(175, 47, 47, 0.2); 314 | } 315 | 316 | .clear-completed, 317 | html .clear-completed:active { 318 | float: right; 319 | position: relative; 320 | line-height: 20px; 321 | text-decoration: none; 322 | cursor: pointer; 323 | position: relative; 324 | } 325 | 326 | .clear-completed:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .info { 331 | margin: 65px auto 0; 332 | color: #bfbfbf; 333 | font-size: 10px; 334 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 335 | text-align: center; 336 | } 337 | 338 | .info p { 339 | line-height: 1; 340 | } 341 | 342 | .info a { 343 | color: inherit; 344 | text-decoration: none; 345 | font-weight: 400; 346 | } 347 | 348 | .info a:hover { 349 | text-decoration: underline; 350 | } 351 | 352 | /* 353 | Hack to remove background from Mobile Safari. 354 | Can't use it globally since it destroys checkboxes in Firefox 355 | */ 356 | @media screen and (-webkit-min-device-pixel-ratio:0) { 357 | .toggle-all, 358 | .todo-list li .toggle { 359 | background: none; 360 | } 361 | 362 | .todo-list li .toggle { 363 | height: 40px; 364 | } 365 | 366 | .toggle-all { 367 | -webkit-transform: rotate(90deg); 368 | transform: rotate(90deg); 369 | -webkit-appearance: none; 370 | appearance: none; 371 | } 372 | } 373 | 374 | @media (max-width: 430px) { 375 | .footer { 376 | height: 50px; 377 | } 378 | 379 | .filters { 380 | bottom: 10px; 381 | } 382 | } -------------------------------------------------------------------------------- /implementations/elm-0.16/Todo.elm: -------------------------------------------------------------------------------- 1 | module Todo where 2 | {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering. 3 | 4 | This application is broken up into four distinct parts: 5 | 6 | 1. Model - a full definition of the application's state 7 | 2. Update - a way to step the application state forward 8 | 3. View - a way to visualize our application state with HTML 9 | 4. Inputs - the signals necessary to manage events 10 | 11 | This clean division of concerns is a core part of Elm. You can read more about 12 | this in the Pong tutorial: http://elm-lang.org/blog/making-pong 13 | 14 | This program is not particularly large, so definitely see the following 15 | for notes on structuring more complex GUIs with Elm: 16 | https://github.com/evancz/elm-architecture-tutorial/ 17 | -} 18 | 19 | import Html exposing (..) 20 | import Html.Attributes exposing (..) 21 | import Html.Events exposing (..) 22 | import Json.Decode as Json 23 | import Signal exposing (Signal, Address) 24 | import String 25 | import Window 26 | 27 | 28 | ---- MODEL ---- 29 | 30 | -- The full application state of our todo app. 31 | type alias Model = 32 | { tasks : List Task 33 | , field : String 34 | , uid : Int 35 | , visibility : String 36 | } 37 | 38 | 39 | type alias Task = 40 | { description : String 41 | , completed : Bool 42 | , editing : Bool 43 | , id : Int 44 | } 45 | 46 | 47 | newTask : String -> Int -> Task 48 | newTask desc id = 49 | { description = desc 50 | , completed = False 51 | , editing = False 52 | , id = id 53 | } 54 | 55 | 56 | emptyModel : Model 57 | emptyModel = 58 | { tasks = [] 59 | , visibility = "All" 60 | , field = "" 61 | , uid = 0 62 | } 63 | 64 | 65 | ---- UPDATE ---- 66 | 67 | -- A description of the kinds of actions that can be performed on the model of 68 | -- our application. See the following for more info on this pattern and 69 | -- some alternatives: https://github.com/evancz/elm-architecture-tutorial/ 70 | type Action 71 | = NoOp 72 | | UpdateField String 73 | | EditingTask Int Bool 74 | | UpdateTask Int String 75 | | Add 76 | | Delete Int 77 | | DeleteComplete 78 | | Check Int Bool 79 | | CheckAll Bool 80 | | ChangeVisibility String 81 | 82 | 83 | -- How we update our Model on a given Action? 84 | update : Action -> Model -> Model 85 | update action model = 86 | case action of 87 | NoOp -> model 88 | 89 | Add -> 90 | { model | 91 | uid = model.uid + 1, 92 | field = "", 93 | tasks = 94 | if String.isEmpty model.field 95 | then model.tasks 96 | else model.tasks ++ [newTask model.field model.uid] 97 | } 98 | 99 | UpdateField str -> 100 | { model | field = str } 101 | 102 | EditingTask id isEditing -> 103 | let updateTask t = if t.id == id then { t | editing = isEditing } else t 104 | in 105 | { model | tasks = List.map updateTask model.tasks } 106 | 107 | UpdateTask id task -> 108 | let updateTask t = if t.id == id then { t | description = task } else t 109 | in 110 | { model | tasks = List.map updateTask model.tasks } 111 | 112 | Delete id -> 113 | { model | tasks = List.filter (\t -> t.id /= id) model.tasks } 114 | 115 | DeleteComplete -> 116 | { model | tasks = List.filter (not << .completed) model.tasks } 117 | 118 | Check id isCompleted -> 119 | let updateTask t = if t.id == id then { t | completed = isCompleted } else t 120 | in 121 | { model | tasks = List.map updateTask model.tasks } 122 | 123 | CheckAll isCompleted -> 124 | let updateTask t = { t | completed = isCompleted } 125 | in 126 | { model | tasks = List.map updateTask model.tasks } 127 | 128 | ChangeVisibility visibility -> 129 | { model | visibility = visibility } 130 | 131 | 132 | ---- VIEW ---- 133 | 134 | view : Address Action -> Model -> Html 135 | view address model = 136 | div 137 | [ class "todomvc-wrapper" 138 | , style [ ("visibility", "hidden") ] 139 | ] 140 | [ section 141 | [ class "todoapp" ] 142 | [ taskEntry address model.field 143 | , taskList address model.visibility model.tasks 144 | , controls address model.visibility model.tasks 145 | ] 146 | , infoFooter 147 | ] 148 | 149 | 150 | onEnter : Address a -> a -> Attribute 151 | onEnter address value = 152 | on "keydown" 153 | (Json.customDecoder keyCode is13) 154 | (\_ -> Signal.message address value) 155 | 156 | 157 | is13 : Int -> Result String () 158 | is13 code = 159 | if code == 13 then Ok () else Err "not the right key code" 160 | 161 | 162 | taskEntry : Address Action -> String -> Html 163 | taskEntry address task = 164 | header 165 | [ class "header" ] 166 | [ h1 [] [ text "todos" ] 167 | , input 168 | [ class "new-todo" 169 | , placeholder "What needs to be done?" 170 | , autofocus True 171 | , value task 172 | , name "newTodo" 173 | , on "input" targetValue (Signal.message address << UpdateField) 174 | , onEnter address Add 175 | ] 176 | [] 177 | ] 178 | 179 | 180 | taskList : Address Action -> String -> List Task -> Html 181 | taskList address visibility tasks = 182 | let isVisible todo = 183 | case visibility of 184 | "Completed" -> todo.completed 185 | "Active" -> not todo.completed 186 | _ -> True 187 | 188 | allCompleted = List.all .completed tasks 189 | 190 | cssVisibility = if List.isEmpty tasks then "hidden" else "visible" 191 | in 192 | section 193 | [ class "main" 194 | , style [ ("visibility", cssVisibility) ] 195 | ] 196 | [ input 197 | [ class "toggle-all" 198 | , type' "checkbox" 199 | , name "toggle" 200 | , checked allCompleted 201 | , onClick address (CheckAll (not allCompleted)) 202 | ] 203 | [] 204 | , label 205 | [ for "toggle-all" ] 206 | [ text "Mark all as complete" ] 207 | , ul 208 | [ class "todo-list" ] 209 | (List.map (todoItem address) (List.filter isVisible tasks)) 210 | ] 211 | 212 | 213 | todoItem : Address Action -> Task -> Html 214 | todoItem address todo = 215 | li 216 | [ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ] 217 | [ div 218 | [ class "view" ] 219 | [ input 220 | [ class "toggle" 221 | , type' "checkbox" 222 | , checked todo.completed 223 | , onClick address (Check todo.id (not todo.completed)) 224 | ] 225 | [] 226 | , label 227 | [ onDoubleClick address (EditingTask todo.id True) ] 228 | [ text todo.description ] 229 | , button 230 | [ class "destroy" 231 | , onClick address (Delete todo.id) 232 | ] 233 | [] 234 | ] 235 | , input 236 | [ class "edit" 237 | , value todo.description 238 | , name "title" 239 | , id ("todo-" ++ toString todo.id) 240 | , on "input" targetValue (Signal.message address << UpdateTask todo.id) 241 | , onBlur address (EditingTask todo.id False) 242 | , onEnter address (EditingTask todo.id False) 243 | ] 244 | [] 245 | ] 246 | 247 | 248 | controls : Address Action -> String -> List Task -> Html 249 | controls address visibility tasks = 250 | let tasksCompleted = List.length (List.filter .completed tasks) 251 | tasksLeft = List.length tasks - tasksCompleted 252 | item_ = if tasksLeft == 1 then " item" else " items" 253 | in 254 | footer 255 | [ class "footer" 256 | , hidden (List.isEmpty tasks) 257 | ] 258 | [ span 259 | [ class "todo-count" ] 260 | [ strong [] [ text (toString tasksLeft) ] 261 | , text (item_ ++ " left") 262 | ] 263 | , ul 264 | [ class "filters" ] 265 | [ visibilitySwap address "#/" "All" visibility 266 | , text " " 267 | , visibilitySwap address "#/active" "Active" visibility 268 | , text " " 269 | , visibilitySwap address "#/completed" "Completed" visibility 270 | ] 271 | , button 272 | [ class "clear-completed" 273 | , hidden (tasksCompleted == 0) 274 | , onClick address DeleteComplete 275 | ] 276 | [ text ("Clear completed (" ++ toString tasksCompleted ++ ")") ] 277 | ] 278 | 279 | 280 | visibilitySwap : Address Action -> String -> String -> String -> Html 281 | visibilitySwap address uri visibility actualVisibility = 282 | li 283 | [ onClick address (ChangeVisibility visibility) ] 284 | [ a [ href uri, classList [("selected", visibility == actualVisibility)] ] [ text visibility ] ] 285 | 286 | 287 | infoFooter : Html 288 | infoFooter = 289 | footer [ class "info" ] 290 | [ p [] [ text "Double-click to edit a todo" ] 291 | , p [] 292 | [ text "Written by " 293 | , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] 294 | ] 295 | , p [] 296 | [ text "Part of " 297 | , a [ href "http://todomvc.com" ] [ text "TodoMVC" ] 298 | ] 299 | ] 300 | 301 | 302 | ---- INPUTS ---- 303 | 304 | -- wire the entire application together 305 | main : Signal Html 306 | main = 307 | Signal.map (view actions.address) model 308 | 309 | 310 | -- manage the model of our application over time 311 | model : Signal Model 312 | model = 313 | Signal.foldp update emptyModel actions.signal 314 | 315 | 316 | -- actions from user input 317 | actions : Signal.Mailbox Action 318 | actions = 319 | Signal.mailbox NoOp 320 | 321 | 322 | port focus : Signal String 323 | port focus = 324 | let needsFocus act = 325 | case act of 326 | EditingTask id bool -> bool 327 | _ -> False 328 | 329 | toSelector act = 330 | case act of 331 | EditingTask id _ -> "#todo-" ++ toString id 332 | _ -> "" 333 | in 334 | actions.signal 335 | |> Signal.filter needsFocus (EditingTask 0 True) 336 | |> Signal.map toSelector 337 | -------------------------------------------------------------------------------- /implementations/elm-0.16/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "TodoMVC created with Elm and elm-html", 4 | "repository": "https://github.com/evancz/elm-todomvc.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 12 | "evancz/elm-html": "4.0.2 <= v < 5.0.0" 13 | }, 14 | "elm-version": "0.16.0 <= v < 0.17.0" 15 | } 16 | -------------------------------------------------------------------------------- /implementations/elm-0.16/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /implementations/elm-0.16/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '❯'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml;utf8,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: '×'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li .edit { 246 | display: none; 247 | } 248 | 249 | .todo-list li.editing:last-child { 250 | margin-bottom: -1px; 251 | } 252 | 253 | .footer { 254 | color: #777; 255 | padding: 10px 15px; 256 | height: 20px; 257 | text-align: center; 258 | border-top: 1px solid #e6e6e6; 259 | } 260 | 261 | .footer:before { 262 | content: ''; 263 | position: absolute; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | height: 50px; 268 | overflow: hidden; 269 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 270 | 0 8px 0 -3px #f6f6f6, 271 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 272 | 0 16px 0 -6px #f6f6f6, 273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | .todo-count { 277 | float: left; 278 | text-align: left; 279 | } 280 | 281 | .todo-count strong { 282 | font-weight: 300; 283 | } 284 | 285 | .filters { 286 | margin: 0; 287 | padding: 0; 288 | list-style: none; 289 | position: absolute; 290 | right: 0; 291 | left: 0; 292 | } 293 | 294 | .filters li { 295 | display: inline; 296 | } 297 | 298 | .filters li a { 299 | color: inherit; 300 | margin: 3px; 301 | padding: 3px 7px; 302 | text-decoration: none; 303 | border: 1px solid transparent; 304 | border-radius: 3px; 305 | } 306 | 307 | .filters li a.selected, 308 | .filters li a:hover { 309 | border-color: rgba(175, 47, 47, 0.1); 310 | } 311 | 312 | .filters li a.selected { 313 | border-color: rgba(175, 47, 47, 0.2); 314 | } 315 | 316 | .clear-completed, 317 | html .clear-completed:active { 318 | float: right; 319 | position: relative; 320 | line-height: 20px; 321 | text-decoration: none; 322 | cursor: pointer; 323 | position: relative; 324 | } 325 | 326 | .clear-completed:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .info { 331 | margin: 65px auto 0; 332 | color: #bfbfbf; 333 | font-size: 10px; 334 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 335 | text-align: center; 336 | } 337 | 338 | .info p { 339 | line-height: 1; 340 | } 341 | 342 | .info a { 343 | color: inherit; 344 | text-decoration: none; 345 | font-weight: 400; 346 | } 347 | 348 | .info a:hover { 349 | text-decoration: underline; 350 | } 351 | 352 | /* 353 | Hack to remove background from Mobile Safari. 354 | Can't use it globally since it destroys checkboxes in Firefox 355 | */ 356 | @media screen and (-webkit-min-device-pixel-ratio:0) { 357 | .toggle-all, 358 | .todo-list li .toggle { 359 | background: none; 360 | } 361 | 362 | .todo-list li .toggle { 363 | height: 40px; 364 | } 365 | 366 | .toggle-all { 367 | -webkit-transform: rotate(90deg); 368 | transform: rotate(90deg); 369 | -webkit-appearance: none; 370 | appearance: none; 371 | } 372 | } 373 | 374 | @media (max-width: 430px) { 375 | .footer { 376 | height: 50px; 377 | } 378 | 379 | .filters { 380 | bottom: 10px; 381 | } 382 | } -------------------------------------------------------------------------------- /implementations/elm-0.17-optimized/Todo.elm: -------------------------------------------------------------------------------- 1 | port module Todo exposing (main) 2 | {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering. 3 | 4 | This application is broken up into three key parts: 5 | 6 | 1. Model - a full definition of the application's state 7 | 2. Update - a way to step the application state forward 8 | 3. View - a way to visualize our application state with HTML 9 | 10 | This clean division of concerns is a core part of Elm. You can read more about 11 | this in 12 | -} 13 | 14 | import Html exposing (..) 15 | import Html.App as App 16 | import Html.Attributes exposing (..) 17 | import Html.Events exposing (..) 18 | import Html.Keyed as Keyed 19 | import Html.Lazy exposing (lazy, lazy2) 20 | import Json.Decode as Json 21 | import String 22 | 23 | 24 | 25 | main : Program Never 26 | main = 27 | App.program 28 | { init = init 29 | , view = view 30 | , update = update 31 | , subscriptions = \_ -> Sub.none 32 | } 33 | 34 | 35 | port focus : String -> Cmd msg 36 | 37 | 38 | 39 | -- MODEL 40 | 41 | 42 | -- The full application state of our todo app. 43 | type alias Model = 44 | { entries : List Entry 45 | , field : String 46 | , uid : Int 47 | , visibility : String 48 | } 49 | 50 | 51 | type alias Entry = 52 | { description : String 53 | , completed : Bool 54 | , editing : Bool 55 | , id : Int 56 | } 57 | 58 | 59 | emptyModel : Model 60 | emptyModel = 61 | { entries = [] 62 | , visibility = "All" 63 | , field = "" 64 | , uid = 0 65 | } 66 | 67 | 68 | newEntry : String -> Int -> Entry 69 | newEntry desc id = 70 | { description = desc 71 | , completed = False 72 | , editing = False 73 | , id = id 74 | } 75 | 76 | 77 | init : ( Model, Cmd Msg ) 78 | init = 79 | emptyModel ! [] 80 | 81 | 82 | 83 | -- UPDATE 84 | 85 | 86 | {-| Users of our app can trigger messages by clicking and typing. These 87 | messages are fed into the `update` function as they occur, letting us react 88 | to them. 89 | -} 90 | type Msg 91 | = NoOp 92 | | UpdateField String 93 | | EditingEntry Int Bool 94 | | UpdateEntry Int String 95 | | Add 96 | | Delete Int 97 | | DeleteComplete 98 | | Check Int Bool 99 | | CheckAll Bool 100 | | ChangeVisibility String 101 | 102 | 103 | -- How we update our Model on a given Msg? 104 | update : Msg -> Model -> ( Model, Cmd Msg ) 105 | update msg model = 106 | case msg of 107 | NoOp -> 108 | model ! [] 109 | 110 | Add -> 111 | { model 112 | | uid = model.uid + 1 113 | , field = "" 114 | , entries = 115 | if String.isEmpty model.field then 116 | model.entries 117 | else 118 | model.entries ++ [newEntry model.field model.uid] 119 | } 120 | ! [] 121 | 122 | UpdateField str -> 123 | { model | field = str } 124 | ! [] 125 | 126 | EditingEntry id isEditing -> 127 | let 128 | updateEntry t = 129 | if t.id == id then { t | editing = isEditing } else t 130 | in 131 | { model | entries = List.map updateEntry model.entries } 132 | ! [ focus ("#todo-" ++ toString id) ] 133 | 134 | UpdateEntry id task -> 135 | let 136 | updateEntry t = 137 | if t.id == id then { t | description = task } else t 138 | in 139 | { model | entries = List.map updateEntry model.entries } 140 | ! [] 141 | 142 | Delete id -> 143 | { model | entries = List.filter (\t -> t.id /= id) model.entries } 144 | ! [] 145 | 146 | DeleteComplete -> 147 | { model | entries = List.filter (not << .completed) model.entries } 148 | ! [] 149 | 150 | Check id isCompleted -> 151 | let 152 | updateEntry t = 153 | if t.id == id then { t | completed = isCompleted } else t 154 | in 155 | { model | entries = List.map updateEntry model.entries } 156 | ! [] 157 | 158 | CheckAll isCompleted -> 159 | let 160 | updateEntry t = 161 | { t | completed = isCompleted } 162 | in 163 | { model | entries = List.map updateEntry model.entries } 164 | ! [] 165 | 166 | ChangeVisibility visibility -> 167 | { model | visibility = visibility } 168 | ! [] 169 | 170 | 171 | 172 | -- VIEW 173 | 174 | 175 | view : Model -> Html Msg 176 | view model = 177 | div 178 | [ class "todomvc-wrapper" 179 | , style [ ("visibility", "hidden") ] 180 | ] 181 | [ section 182 | [ class "todoapp" ] 183 | [ lazy viewInput model.field 184 | , lazy2 viewEntries model.visibility model.entries 185 | , lazy2 viewControls model.visibility model.entries 186 | ] 187 | , infoFooter 188 | ] 189 | 190 | 191 | viewInput : String -> Html Msg 192 | viewInput task = 193 | header 194 | [ class "header" ] 195 | [ h1 [] [ text "todos" ] 196 | , input 197 | [ class "new-todo" 198 | , placeholder "What needs to be done?" 199 | , autofocus True 200 | , value task 201 | , name "newTodo" 202 | , onInput UpdateField 203 | , onEnter Add 204 | ] 205 | [] 206 | ] 207 | 208 | 209 | onEnter : Msg -> Attribute Msg 210 | onEnter msg = 211 | let 212 | tagger code = 213 | if code == 13 then msg else NoOp 214 | in 215 | on "keydown" (Json.map tagger keyCode) 216 | 217 | 218 | 219 | -- VIEW ALL ENTRIES 220 | 221 | 222 | viewEntries : String -> List Entry -> Html Msg 223 | viewEntries visibility entries = 224 | let 225 | isVisible todo = 226 | case visibility of 227 | "Completed" -> todo.completed 228 | "Active" -> not todo.completed 229 | _ -> True 230 | 231 | allCompleted = 232 | List.all .completed entries 233 | 234 | cssVisibility = 235 | if List.isEmpty entries then "hidden" else "visible" 236 | in 237 | section 238 | [ class "main" 239 | , style [ ("visibility", cssVisibility) ] 240 | ] 241 | [ input 242 | [ class "toggle-all" 243 | , type' "checkbox" 244 | , name "toggle" 245 | , checked allCompleted 246 | , onClick (CheckAll (not allCompleted)) 247 | ] 248 | [] 249 | , label 250 | [ for "toggle-all" ] 251 | [ text "Mark all as complete" ] 252 | , Keyed.ul [ class "todo-list" ] <| 253 | List.map viewKeyedEntry (List.filter isVisible entries) 254 | ] 255 | 256 | 257 | 258 | -- VIEW INDIVIDUAL ENTRIES 259 | 260 | 261 | viewKeyedEntry : Entry -> (String, Html Msg) 262 | viewKeyedEntry todo = 263 | ( toString todo.id, lazy viewEntry todo ) 264 | 265 | 266 | viewEntry : Entry -> Html Msg 267 | viewEntry todo = 268 | li 269 | [ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ] 270 | [ div 271 | [ class "view" ] 272 | [ input 273 | [ class "toggle" 274 | , type' "checkbox" 275 | , checked todo.completed 276 | , onClick (Check todo.id (not todo.completed)) 277 | ] 278 | [] 279 | , label 280 | [ onDoubleClick (EditingEntry todo.id True) ] 281 | [ text todo.description ] 282 | , button 283 | [ class "destroy" 284 | , onClick (Delete todo.id) 285 | ] 286 | [] 287 | ] 288 | , input 289 | [ class "edit" 290 | , value todo.description 291 | , name "title" 292 | , id ("todo-" ++ toString todo.id) 293 | , onInput (UpdateEntry todo.id) 294 | , onBlur (EditingEntry todo.id False) 295 | , onEnter (EditingEntry todo.id False) 296 | ] 297 | [] 298 | ] 299 | 300 | 301 | 302 | -- VIEW CONTROLS AND FOOTER 303 | 304 | 305 | viewControls : String -> List Entry -> Html Msg 306 | viewControls visibility entries = 307 | let 308 | entriesCompleted = 309 | List.length (List.filter .completed entries) 310 | 311 | entriesLeft = 312 | List.length entries - entriesCompleted 313 | in 314 | footer 315 | [ class "footer" 316 | , hidden (List.isEmpty entries) 317 | ] 318 | [ lazy viewControlsCount entriesLeft 319 | , lazy viewControlsFilters visibility 320 | , lazy viewControlsClear entriesCompleted 321 | ] 322 | 323 | 324 | viewControlsCount : Int -> Html Msg 325 | viewControlsCount entriesLeft = 326 | let 327 | item_ = 328 | if entriesLeft == 1 then " item" else " items" 329 | in 330 | span 331 | [ class "todo-count" ] 332 | [ strong [] [ text (toString entriesLeft) ] 333 | , text (item_ ++ " left") 334 | ] 335 | 336 | 337 | viewControlsFilters : String -> Html Msg 338 | viewControlsFilters visibility = 339 | ul 340 | [ class "filters" ] 341 | [ visibilitySwap "#/" "All" visibility 342 | , text " " 343 | , visibilitySwap "#/active" "Active" visibility 344 | , text " " 345 | , visibilitySwap "#/completed" "Completed" visibility 346 | ] 347 | 348 | 349 | visibilitySwap : String -> String -> String -> Html Msg 350 | visibilitySwap uri visibility actualVisibility = 351 | li 352 | [ onClick (ChangeVisibility visibility) ] 353 | [ a [ href uri, classList [("selected", visibility == actualVisibility)] ] 354 | [ text visibility ] 355 | ] 356 | 357 | 358 | viewControlsClear : Int -> Html Msg 359 | viewControlsClear entriesCompleted = 360 | button 361 | [ class "clear-completed" 362 | , hidden (entriesCompleted == 0) 363 | , onClick DeleteComplete 364 | ] 365 | [ text ("Clear completed (" ++ toString entriesCompleted ++ ")") 366 | ] 367 | 368 | 369 | infoFooter : Html msg 370 | infoFooter = 371 | footer [ class "info" ] 372 | [ p [] [ text "Double-click to edit a todo" ] 373 | , p [] 374 | [ text "Written by " 375 | , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] 376 | ] 377 | , p [] 378 | [ text "Part of " 379 | , a [ href "http://todomvc.com" ] [ text "TodoMVC" ] 380 | ] 381 | ] 382 | -------------------------------------------------------------------------------- /implementations/elm-0.17-optimized/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "TodoMVC created with Elm and elm-html", 4 | "repository": "https://github.com/evancz/elm-todomvc.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 12 | "elm-lang/html": "1.0.0 <= v < 2.0.0" 13 | }, 14 | "elm-version": "0.17.0 <= v < 0.18.0" 15 | } 16 | -------------------------------------------------------------------------------- /implementations/elm-0.17-optimized/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /implementations/elm-0.17-optimized/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '❯'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml;utf8,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: '×'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li .edit { 246 | display: none; 247 | } 248 | 249 | .todo-list li.editing:last-child { 250 | margin-bottom: -1px; 251 | } 252 | 253 | .footer { 254 | color: #777; 255 | padding: 10px 15px; 256 | height: 20px; 257 | text-align: center; 258 | border-top: 1px solid #e6e6e6; 259 | } 260 | 261 | .footer:before { 262 | content: ''; 263 | position: absolute; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | height: 50px; 268 | overflow: hidden; 269 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 270 | 0 8px 0 -3px #f6f6f6, 271 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 272 | 0 16px 0 -6px #f6f6f6, 273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | .todo-count { 277 | float: left; 278 | text-align: left; 279 | } 280 | 281 | .todo-count strong { 282 | font-weight: 300; 283 | } 284 | 285 | .filters { 286 | margin: 0; 287 | padding: 0; 288 | list-style: none; 289 | position: absolute; 290 | right: 0; 291 | left: 0; 292 | } 293 | 294 | .filters li { 295 | display: inline; 296 | } 297 | 298 | .filters li a { 299 | color: inherit; 300 | margin: 3px; 301 | padding: 3px 7px; 302 | text-decoration: none; 303 | border: 1px solid transparent; 304 | border-radius: 3px; 305 | } 306 | 307 | .filters li a.selected, 308 | .filters li a:hover { 309 | border-color: rgba(175, 47, 47, 0.1); 310 | } 311 | 312 | .filters li a.selected { 313 | border-color: rgba(175, 47, 47, 0.2); 314 | } 315 | 316 | .clear-completed, 317 | html .clear-completed:active { 318 | float: right; 319 | position: relative; 320 | line-height: 20px; 321 | text-decoration: none; 322 | cursor: pointer; 323 | position: relative; 324 | } 325 | 326 | .clear-completed:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .info { 331 | margin: 65px auto 0; 332 | color: #bfbfbf; 333 | font-size: 10px; 334 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 335 | text-align: center; 336 | } 337 | 338 | .info p { 339 | line-height: 1; 340 | } 341 | 342 | .info a { 343 | color: inherit; 344 | text-decoration: none; 345 | font-weight: 400; 346 | } 347 | 348 | .info a:hover { 349 | text-decoration: underline; 350 | } 351 | 352 | /* 353 | Hack to remove background from Mobile Safari. 354 | Can't use it globally since it destroys checkboxes in Firefox 355 | */ 356 | @media screen and (-webkit-min-device-pixel-ratio:0) { 357 | .toggle-all, 358 | .todo-list li .toggle { 359 | background: none; 360 | } 361 | 362 | .todo-list li .toggle { 363 | height: 40px; 364 | } 365 | 366 | .toggle-all { 367 | -webkit-transform: rotate(90deg); 368 | transform: rotate(90deg); 369 | -webkit-appearance: none; 370 | appearance: none; 371 | } 372 | } 373 | 374 | @media (max-width: 430px) { 375 | .footer { 376 | height: 50px; 377 | } 378 | 379 | .filters { 380 | bottom: 10px; 381 | } 382 | } -------------------------------------------------------------------------------- /implementations/elm-0.17/Todo.elm: -------------------------------------------------------------------------------- 1 | port module Todo exposing (main) 2 | {-| TodoMVC implemented in Elm, using plain HTML and CSS for rendering. 3 | 4 | This application is broken up into three key parts: 5 | 6 | 1. Model - a full definition of the application's state 7 | 2. Update - a way to step the application state forward 8 | 3. View - a way to visualize our application state with HTML 9 | 10 | This clean division of concerns is a core part of Elm. You can read more about 11 | this in 12 | -} 13 | 14 | import Html exposing (..) 15 | import Html.App as App 16 | import Html.Attributes exposing (..) 17 | import Html.Events exposing (..) 18 | import Json.Decode as Json 19 | import String 20 | 21 | 22 | 23 | main : Program Never 24 | main = 25 | App.program 26 | { init = init 27 | , view = view 28 | , update = update 29 | , subscriptions = \_ -> Sub.none 30 | } 31 | 32 | 33 | port focus : String -> Cmd msg 34 | 35 | 36 | 37 | -- MODEL 38 | 39 | 40 | -- The full application state of our todo app. 41 | type alias Model = 42 | { entries : List Entry 43 | , field : String 44 | , uid : Int 45 | , visibility : String 46 | } 47 | 48 | 49 | type alias Entry = 50 | { description : String 51 | , completed : Bool 52 | , editing : Bool 53 | , id : Int 54 | } 55 | 56 | 57 | emptyModel : Model 58 | emptyModel = 59 | { entries = [] 60 | , visibility = "All" 61 | , field = "" 62 | , uid = 0 63 | } 64 | 65 | 66 | newEntry : String -> Int -> Entry 67 | newEntry desc id = 68 | { description = desc 69 | , completed = False 70 | , editing = False 71 | , id = id 72 | } 73 | 74 | 75 | init : ( Model, Cmd Msg ) 76 | init = 77 | emptyModel ! [] 78 | 79 | 80 | 81 | -- UPDATE 82 | 83 | 84 | {-| Users of our app can trigger messages by clicking and typing. These 85 | messages are fed into the `update` function as they occur, letting us react 86 | to them. 87 | -} 88 | type Msg 89 | = NoOp 90 | | UpdateField String 91 | | EditingEntry Int Bool 92 | | UpdateEntry Int String 93 | | Add 94 | | Delete Int 95 | | DeleteComplete 96 | | Check Int Bool 97 | | CheckAll Bool 98 | | ChangeVisibility String 99 | 100 | 101 | -- How we update our Model on a given Msg? 102 | update : Msg -> Model -> ( Model, Cmd Msg ) 103 | update msg model = 104 | case msg of 105 | NoOp -> 106 | model ! [] 107 | 108 | Add -> 109 | { model 110 | | uid = model.uid + 1 111 | , field = "" 112 | , entries = 113 | if String.isEmpty model.field then 114 | model.entries 115 | else 116 | model.entries ++ [newEntry model.field model.uid] 117 | } 118 | ! [] 119 | 120 | UpdateField str -> 121 | { model | field = str } 122 | ! [] 123 | 124 | EditingEntry id isEditing -> 125 | let 126 | updateEntry t = 127 | if t.id == id then { t | editing = isEditing } else t 128 | in 129 | { model | entries = List.map updateEntry model.entries } 130 | ! [ focus ("#todo-" ++ toString id) ] 131 | 132 | UpdateEntry id task -> 133 | let 134 | updateEntry t = 135 | if t.id == id then { t | description = task } else t 136 | in 137 | { model | entries = List.map updateEntry model.entries } 138 | ! [] 139 | 140 | Delete id -> 141 | { model | entries = List.filter (\t -> t.id /= id) model.entries } 142 | ! [] 143 | 144 | DeleteComplete -> 145 | { model | entries = List.filter (not << .completed) model.entries } 146 | ! [] 147 | 148 | Check id isCompleted -> 149 | let 150 | updateEntry t = 151 | if t.id == id then { t | completed = isCompleted } else t 152 | in 153 | { model | entries = List.map updateEntry model.entries } 154 | ! [] 155 | 156 | CheckAll isCompleted -> 157 | let 158 | updateEntry t = 159 | { t | completed = isCompleted } 160 | in 161 | { model | entries = List.map updateEntry model.entries } 162 | ! [] 163 | 164 | ChangeVisibility visibility -> 165 | { model | visibility = visibility } 166 | ! [] 167 | 168 | 169 | 170 | -- VIEW 171 | 172 | 173 | view : Model -> Html Msg 174 | view model = 175 | div 176 | [ class "todomvc-wrapper" 177 | , style [ ("visibility", "hidden") ] 178 | ] 179 | [ section 180 | [ class "todoapp" ] 181 | [ viewInput model.field 182 | , viewEntries model.visibility model.entries 183 | , viewControls model.visibility model.entries 184 | ] 185 | , infoFooter 186 | ] 187 | 188 | 189 | viewInput : String -> Html Msg 190 | viewInput task = 191 | header 192 | [ class "header" ] 193 | [ h1 [] [ text "todos" ] 194 | , input 195 | [ class "new-todo" 196 | , placeholder "What needs to be done?" 197 | , autofocus True 198 | , value task 199 | , name "newTodo" 200 | , onInput UpdateField 201 | , onEnter Add 202 | ] 203 | [] 204 | ] 205 | 206 | 207 | onEnter : Msg -> Attribute Msg 208 | onEnter msg = 209 | let 210 | tagger code = 211 | if code == 13 then msg else NoOp 212 | in 213 | on "keydown" (Json.map tagger keyCode) 214 | 215 | 216 | 217 | -- VIEW ALL ENTRIES 218 | 219 | 220 | viewEntries : String -> List Entry -> Html Msg 221 | viewEntries visibility entries = 222 | let 223 | isVisible todo = 224 | case visibility of 225 | "Completed" -> todo.completed 226 | "Active" -> not todo.completed 227 | _ -> True 228 | 229 | allCompleted = 230 | List.all .completed entries 231 | 232 | cssVisibility = 233 | if List.isEmpty entries then "hidden" else "visible" 234 | in 235 | section 236 | [ class "main" 237 | , style [ ("visibility", cssVisibility) ] 238 | ] 239 | [ input 240 | [ class "toggle-all" 241 | , type' "checkbox" 242 | , name "toggle" 243 | , checked allCompleted 244 | , onClick (CheckAll (not allCompleted)) 245 | ] 246 | [] 247 | , label 248 | [ for "toggle-all" ] 249 | [ text "Mark all as complete" ] 250 | , ul [ class "todo-list" ] <| 251 | List.map viewEntry (List.filter isVisible entries) 252 | ] 253 | 254 | 255 | 256 | -- VIEW INDIVIDUAL ENTRIES 257 | 258 | 259 | viewEntry : Entry -> Html Msg 260 | viewEntry todo = 261 | li 262 | [ classList [ ("completed", todo.completed), ("editing", todo.editing) ] ] 263 | [ div 264 | [ class "view" ] 265 | [ input 266 | [ class "toggle" 267 | , type' "checkbox" 268 | , checked todo.completed 269 | , onClick (Check todo.id (not todo.completed)) 270 | ] 271 | [] 272 | , label 273 | [ onDoubleClick (EditingEntry todo.id True) ] 274 | [ text todo.description ] 275 | , button 276 | [ class "destroy" 277 | , onClick (Delete todo.id) 278 | ] 279 | [] 280 | ] 281 | , input 282 | [ class "edit" 283 | , value todo.description 284 | , name "title" 285 | , id ("todo-" ++ toString todo.id) 286 | , onInput (UpdateEntry todo.id) 287 | , onBlur (EditingEntry todo.id False) 288 | , onEnter (EditingEntry todo.id False) 289 | ] 290 | [] 291 | ] 292 | 293 | 294 | 295 | -- VIEW CONTROLS AND FOOTER 296 | 297 | 298 | viewControls : String -> List Entry -> Html Msg 299 | viewControls visibility entries = 300 | let 301 | entriesCompleted = 302 | List.length (List.filter .completed entries) 303 | 304 | entriesLeft = 305 | List.length entries - entriesCompleted 306 | in 307 | footer 308 | [ class "footer" 309 | , hidden (List.isEmpty entries) 310 | ] 311 | [ viewControlsCount entriesLeft 312 | , viewControlsFilters visibility 313 | , viewControlsClear entriesCompleted 314 | ] 315 | 316 | 317 | viewControlsCount : Int -> Html Msg 318 | viewControlsCount entriesLeft = 319 | let 320 | item_ = 321 | if entriesLeft == 1 then " item" else " items" 322 | in 323 | span 324 | [ class "todo-count" ] 325 | [ strong [] [ text (toString entriesLeft) ] 326 | , text (item_ ++ " left") 327 | ] 328 | 329 | 330 | viewControlsFilters : String -> Html Msg 331 | viewControlsFilters visibility = 332 | ul 333 | [ class "filters" ] 334 | [ visibilitySwap "#/" "All" visibility 335 | , text " " 336 | , visibilitySwap "#/active" "Active" visibility 337 | , text " " 338 | , visibilitySwap "#/completed" "Completed" visibility 339 | ] 340 | 341 | 342 | visibilitySwap : String -> String -> String -> Html Msg 343 | visibilitySwap uri visibility actualVisibility = 344 | li 345 | [ onClick (ChangeVisibility visibility) ] 346 | [ a [ href uri, classList [("selected", visibility == actualVisibility)] ] 347 | [ text visibility ] 348 | ] 349 | 350 | 351 | viewControlsClear : Int -> Html Msg 352 | viewControlsClear entriesCompleted = 353 | button 354 | [ class "clear-completed" 355 | , hidden (entriesCompleted == 0) 356 | , onClick DeleteComplete 357 | ] 358 | [ text ("Clear completed (" ++ toString entriesCompleted ++ ")") 359 | ] 360 | 361 | 362 | infoFooter : Html msg 363 | infoFooter = 364 | footer [ class "info" ] 365 | [ p [] [ text "Double-click to edit a todo" ] 366 | , p [] 367 | [ text "Written by " 368 | , a [ href "https://github.com/evancz" ] [ text "Evan Czaplicki" ] 369 | ] 370 | , p [] 371 | [ text "Part of " 372 | , a [ href "http://todomvc.com" ] [ text "TodoMVC" ] 373 | ] 374 | ] 375 | -------------------------------------------------------------------------------- /implementations/elm-0.17/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "TodoMVC created with Elm and elm-html", 4 | "repository": "https://github.com/evancz/elm-todomvc.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 12 | "elm-lang/html": "1.0.0 <= v < 2.0.0" 13 | }, 14 | "elm-version": "0.17.0 <= v < 0.18.0" 15 | } 16 | -------------------------------------------------------------------------------- /implementations/elm-0.17/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Elm • TodoMVC 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /implementations/elm-0.17/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | .todomvc-wrapper { 8 | visibility: visible !important; 9 | } 10 | 11 | button { 12 | margin: 0; 13 | padding: 0; 14 | border: 0; 15 | background: none; 16 | font-size: 100%; 17 | vertical-align: baseline; 18 | font-family: inherit; 19 | font-weight: inherit; 20 | color: inherit; 21 | -webkit-appearance: none; 22 | appearance: none; 23 | -webkit-font-smoothing: antialiased; 24 | -moz-font-smoothing: antialiased; 25 | font-smoothing: antialiased; 26 | } 27 | 28 | body { 29 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 30 | line-height: 1.4em; 31 | background: #f5f5f5; 32 | color: #4d4d4d; 33 | min-width: 230px; 34 | max-width: 550px; 35 | margin: 0 auto; 36 | -webkit-font-smoothing: antialiased; 37 | -moz-font-smoothing: antialiased; 38 | font-smoothing: antialiased; 39 | font-weight: 300; 40 | } 41 | 42 | button, 43 | input[type="checkbox"] { 44 | outline: none; 45 | } 46 | 47 | .hidden { 48 | display: none; 49 | } 50 | 51 | .todoapp { 52 | background: #fff; 53 | margin: 130px 0 40px 0; 54 | position: relative; 55 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 56 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 57 | } 58 | 59 | .todoapp input::-webkit-input-placeholder { 60 | font-style: italic; 61 | font-weight: 300; 62 | color: #e6e6e6; 63 | } 64 | 65 | .todoapp input::-moz-placeholder { 66 | font-style: italic; 67 | font-weight: 300; 68 | color: #e6e6e6; 69 | } 70 | 71 | .todoapp input::input-placeholder { 72 | font-style: italic; 73 | font-weight: 300; 74 | color: #e6e6e6; 75 | } 76 | 77 | .todoapp h1 { 78 | position: absolute; 79 | top: -155px; 80 | width: 100%; 81 | font-size: 100px; 82 | font-weight: 100; 83 | text-align: center; 84 | color: rgba(175, 47, 47, 0.15); 85 | -webkit-text-rendering: optimizeLegibility; 86 | -moz-text-rendering: optimizeLegibility; 87 | text-rendering: optimizeLegibility; 88 | } 89 | 90 | .new-todo, 91 | .edit { 92 | position: relative; 93 | margin: 0; 94 | width: 100%; 95 | font-size: 24px; 96 | font-family: inherit; 97 | font-weight: inherit; 98 | line-height: 1.4em; 99 | border: 0; 100 | outline: none; 101 | color: inherit; 102 | padding: 6px; 103 | border: 1px solid #999; 104 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 105 | box-sizing: border-box; 106 | -webkit-font-smoothing: antialiased; 107 | -moz-font-smoothing: antialiased; 108 | font-smoothing: antialiased; 109 | } 110 | 111 | .new-todo { 112 | padding: 16px 16px 16px 60px; 113 | border: none; 114 | background: rgba(0, 0, 0, 0.003); 115 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 116 | } 117 | 118 | .main { 119 | position: relative; 120 | z-index: 2; 121 | border-top: 1px solid #e6e6e6; 122 | } 123 | 124 | label[for='toggle-all'] { 125 | display: none; 126 | } 127 | 128 | .toggle-all { 129 | position: absolute; 130 | top: -55px; 131 | left: -12px; 132 | width: 60px; 133 | height: 34px; 134 | text-align: center; 135 | border: none; /* Mobile Safari */ 136 | } 137 | 138 | .toggle-all:before { 139 | content: '❯'; 140 | font-size: 22px; 141 | color: #e6e6e6; 142 | padding: 10px 27px 10px 27px; 143 | } 144 | 145 | .toggle-all:checked:before { 146 | color: #737373; 147 | } 148 | 149 | .todo-list { 150 | margin: 0; 151 | padding: 0; 152 | list-style: none; 153 | } 154 | 155 | .todo-list li { 156 | position: relative; 157 | font-size: 24px; 158 | border-bottom: 1px solid #ededed; 159 | } 160 | 161 | .todo-list li:last-child { 162 | border-bottom: none; 163 | } 164 | 165 | .todo-list li.editing { 166 | border-bottom: none; 167 | padding: 0; 168 | } 169 | 170 | .todo-list li.editing .edit { 171 | display: block; 172 | width: 506px; 173 | padding: 13px 17px 12px 17px; 174 | margin: 0 0 0 43px; 175 | } 176 | 177 | .todo-list li.editing .view { 178 | display: none; 179 | } 180 | 181 | .todo-list li .toggle { 182 | text-align: center; 183 | width: 40px; 184 | /* auto, since non-WebKit browsers doesn't support input styling */ 185 | height: auto; 186 | position: absolute; 187 | top: 0; 188 | bottom: 0; 189 | margin: auto 0; 190 | border: none; /* Mobile Safari */ 191 | -webkit-appearance: none; 192 | appearance: none; 193 | } 194 | 195 | .todo-list li .toggle:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | .todo-list li .toggle:checked:after { 200 | content: url('data:image/svg+xml;utf8,'); 201 | } 202 | 203 | .todo-list li label { 204 | white-space: pre-line; 205 | word-break: break-all; 206 | padding: 15px 60px 15px 15px; 207 | margin-left: 45px; 208 | display: block; 209 | line-height: 1.2; 210 | transition: color 0.4s; 211 | } 212 | 213 | .todo-list li.completed label { 214 | color: #d9d9d9; 215 | text-decoration: line-through; 216 | } 217 | 218 | .todo-list li .destroy { 219 | display: none; 220 | position: absolute; 221 | top: 0; 222 | right: 10px; 223 | bottom: 0; 224 | width: 40px; 225 | height: 40px; 226 | margin: auto 0; 227 | font-size: 30px; 228 | color: #cc9a9a; 229 | margin-bottom: 11px; 230 | transition: color 0.2s ease-out; 231 | } 232 | 233 | .todo-list li .destroy:hover { 234 | color: #af5b5e; 235 | } 236 | 237 | .todo-list li .destroy:after { 238 | content: '×'; 239 | } 240 | 241 | .todo-list li:hover .destroy { 242 | display: block; 243 | } 244 | 245 | .todo-list li .edit { 246 | display: none; 247 | } 248 | 249 | .todo-list li.editing:last-child { 250 | margin-bottom: -1px; 251 | } 252 | 253 | .footer { 254 | color: #777; 255 | padding: 10px 15px; 256 | height: 20px; 257 | text-align: center; 258 | border-top: 1px solid #e6e6e6; 259 | } 260 | 261 | .footer:before { 262 | content: ''; 263 | position: absolute; 264 | right: 0; 265 | bottom: 0; 266 | left: 0; 267 | height: 50px; 268 | overflow: hidden; 269 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 270 | 0 8px 0 -3px #f6f6f6, 271 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 272 | 0 16px 0 -6px #f6f6f6, 273 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 274 | } 275 | 276 | .todo-count { 277 | float: left; 278 | text-align: left; 279 | } 280 | 281 | .todo-count strong { 282 | font-weight: 300; 283 | } 284 | 285 | .filters { 286 | margin: 0; 287 | padding: 0; 288 | list-style: none; 289 | position: absolute; 290 | right: 0; 291 | left: 0; 292 | } 293 | 294 | .filters li { 295 | display: inline; 296 | } 297 | 298 | .filters li a { 299 | color: inherit; 300 | margin: 3px; 301 | padding: 3px 7px; 302 | text-decoration: none; 303 | border: 1px solid transparent; 304 | border-radius: 3px; 305 | } 306 | 307 | .filters li a.selected, 308 | .filters li a:hover { 309 | border-color: rgba(175, 47, 47, 0.1); 310 | } 311 | 312 | .filters li a.selected { 313 | border-color: rgba(175, 47, 47, 0.2); 314 | } 315 | 316 | .clear-completed, 317 | html .clear-completed:active { 318 | float: right; 319 | position: relative; 320 | line-height: 20px; 321 | text-decoration: none; 322 | cursor: pointer; 323 | position: relative; 324 | } 325 | 326 | .clear-completed:hover { 327 | text-decoration: underline; 328 | } 329 | 330 | .info { 331 | margin: 65px auto 0; 332 | color: #bfbfbf; 333 | font-size: 10px; 334 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 335 | text-align: center; 336 | } 337 | 338 | .info p { 339 | line-height: 1; 340 | } 341 | 342 | .info a { 343 | color: inherit; 344 | text-decoration: none; 345 | font-weight: 400; 346 | } 347 | 348 | .info a:hover { 349 | text-decoration: underline; 350 | } 351 | 352 | /* 353 | Hack to remove background from Mobile Safari. 354 | Can't use it globally since it destroys checkboxes in Firefox 355 | */ 356 | @media screen and (-webkit-min-device-pixel-ratio:0) { 357 | .toggle-all, 358 | .todo-list li .toggle { 359 | background: none; 360 | } 361 | 362 | .todo-list li .toggle { 363 | height: 40px; 364 | } 365 | 366 | .toggle-all { 367 | -webkit-transform: rotate(90deg); 368 | transform: rotate(90deg); 369 | -webkit-appearance: none; 370 | appearance: none; 371 | } 372 | } 373 | 374 | @media (max-width: 430px) { 375 | .footer { 376 | height: 50px; 377 | } 378 | 379 | .filters { 380 | bottom: 10px; 381 | } 382 | } -------------------------------------------------------------------------------- /implementations/ember-2.6.3/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/README.md: -------------------------------------------------------------------------------- 1 | # Ember.js TodoMVC for Benchmarking 2 | 3 | This is an update of the 4 | _[Ember.js TodoMVC example](https://github.com/tastejs/todomvc/tree/gh-pages/examples/emberjs)_ 5 | with the following changes: 6 | 7 | * Upgrade to Ember 2.6.2 8 | * Configure router to support relative URLs 9 | * Remove localStorage functionality - not relevant to measuring render performance 10 | 11 | # Building 12 | 13 | 1. `npm install` 14 | 2. `npm run make` 15 | 3. `open dist/index.html` 16 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/components/todo-item.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | repo: Ember.inject.service(), 5 | tagName: 'li', 6 | editing: false, 7 | classNameBindings: ['todo.completed', 'editing'], 8 | 9 | actions: { 10 | startEditing() { 11 | this.get('onStartEdit')(); 12 | this.set('editing', true); 13 | Ember.run.scheduleOnce('afterRender', this, 'focusInput'); 14 | }, 15 | 16 | doneEditing(todoTitle) { 17 | if (!this.get('editing')) { return; } 18 | if (Ember.isBlank(todoTitle)) { 19 | this.send('removeTodo'); 20 | } else { 21 | this.set('todo.title', todoTitle.trim()); 22 | this.set('editing', false); 23 | this.get('onEndEdit')(); 24 | } 25 | }, 26 | 27 | handleKeydown(e) { 28 | if (e.keyCode === 13) { 29 | e.target.blur(); 30 | } else if (e.keyCode === 27) { 31 | this.set('editing', false); 32 | } 33 | }, 34 | 35 | toggleCompleted(e) { 36 | let todo = this.get('todo'); 37 | Ember.set(todo, 'completed', e.target.checked); 38 | }, 39 | 40 | removeTodo() { 41 | this.get('repo').delete(this.get('todo')); 42 | } 43 | }, 44 | 45 | focusInput() { 46 | this.element.querySelector('input.edit').focus(); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/components/todo-list.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Component.extend({ 4 | repo: Ember.inject.service(), 5 | tagName: 'section', 6 | elementId: 'main', 7 | canToggle: true, 8 | allCompleted: Ember.computed('todos.@each.completed', function () { 9 | return this.get('todos').isEvery('completed'); 10 | }), 11 | 12 | actions: { 13 | enableToggle() { 14 | this.set('canToggle', true); 15 | }, 16 | 17 | disableToggle() { 18 | this.set('canToggle', false); 19 | }, 20 | 21 | toggleAll() { 22 | let allCompleted = this.get('allCompleted'); 23 | this.get('todos').forEach(todo => Ember.set(todo, 'completed', !allCompleted)); 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/controllers/active.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | todos: Ember.computed.filterBy('model', 'completed', false) 5 | }); 6 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | repo: Ember.inject.service(), 5 | remaining: Ember.computed.filterBy('model', 'completed', false), 6 | completed: Ember.computed.filterBy('model', 'completed'), 7 | actions: { 8 | createTodo(e) { 9 | if (e.keyCode === 13 && !Ember.isBlank(e.target.value)) { 10 | this.get('repo').add({ title: e.target.value.trim(), completed: false }); 11 | e.target.value = ''; 12 | } 13 | }, 14 | 15 | clearCompleted() { 16 | this.get('model').removeObjects(this.get('completed')); 17 | } 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/controllers/completed.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | todos: Ember.computed.filterBy('model', 'completed', true) 5 | }); 6 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/helpers/gt.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export function gt([n1, n2]/*, hash*/) { 4 | return n1 > n2; 5 | } 6 | 7 | export default Ember.Helper.helper(gt); 8 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/helpers/pluralize.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { pluralize } from 'ember-inflector'; 3 | 4 | export function pluralizeHelper([singular, count]/*, hash*/) { 5 | return count === 1 ? singular : pluralize(singular); 6 | } 7 | 8 | export default Ember.Helper.helper(pluralizeHelper); 9 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Todomvc 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function () { 9 | this.route('active'); 10 | this.route('completed'); 11 | }); 12 | 13 | export default Router; 14 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | repo: Ember.inject.service(), 5 | model() { 6 | return this.get('repo').findAll(); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/services/repo.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | lastId: 0, 5 | data: null, 6 | findAll() { 7 | return this.get('data') || this.set('data', []); 8 | }, 9 | 10 | add(attrs) { 11 | let todo = Object.assign({ id: this.incrementProperty('lastId') }, attrs); 12 | this.get('data').pushObject(todo); 13 | return todo; 14 | }, 15 | 16 | delete(todo) { 17 | this.get('data').removeObject(todo); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evancz/react-angular-ember-elm-performance-comparison/6633af5a172dc43bed3bb4ce01bbbd0e32127a75/implementations/ember-2.6.3/app/styles/app.css -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/active.hbs: -------------------------------------------------------------------------------- 1 | {{todo-list todos=todos}} -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 | 6 | {{outlet}} 7 | {{#if (gt model.length 0)}} 8 |
9 | {{remaining.length}} {{pluralize 'item' remaining.length}} left 10 |
    11 |
  • {{#link-to "index" activeClass="selected"}}All{{/link-to}}
  • 12 |
  • {{#link-to "active" activeClass="selected"}}Active{{/link-to}}
  • 13 |
  • {{#link-to "completed" activeClass="selected"}}Completed{{/link-to}}
  • 14 |
15 | {{#if completed.length}} 16 | 17 | {{/if}} 18 |
19 | {{/if}} 20 |
21 | 30 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/completed.hbs: -------------------------------------------------------------------------------- 1 | {{todo-list todos=todos}} -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/components/todo-item.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/components/todo-list.hbs: -------------------------------------------------------------------------------- 1 | {{#if todos.length}} 2 | {{#if canToggle}} 3 | 4 | {{/if}} 5 |
    6 | {{#each todos as |todo|}} 7 | {{todo-item todo=todo onStartEdit=(action 'disableToggle') onEndEdit=(action 'enableToggle')}} 8 | {{/each}} 9 |
10 | {{/if}} 11 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 | {{#if model.length}} 2 | {{todo-list todos=model}} 3 | {{/if}} 4 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "dependencies": { 4 | "ember": "~2.6.2", 5 | "ember-cli-shims": "0.1.1", 6 | "ember-cli-test-loader": "0.2.2", 7 | "ember-qunit-notifications": "0.1.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function (environment) { 4 | var ENV = { 5 | modulePrefix: 'todomvc', 6 | environment: environment, 7 | baseURL: null, 8 | locationType: 'none', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | } 14 | }, 15 | 16 | APP: { 17 | // Here you can pass flags/options to your application instance 18 | // when it is created 19 | } 20 | }; 21 | 22 | // if (environment === 'development') { 23 | // ENV.APP.LOG_RESOLVER = true; 24 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 25 | // ENV.APP.LOG_TRANSITIONS = true; 26 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 27 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 28 | // } 29 | 30 | if (environment === 'test') { 31 | // Testem prefers this... 32 | ENV.baseURL = '/'; 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | } 41 | 42 | // if (environment === 'production') { 43 | 44 | // } 45 | 46 | return ENV; 47 | }; 48 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | /* global require, module */ 3 | var EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | var app = new EmberApp(defaults, { 7 | // Add options here 8 | }); 9 | 10 | app.import('vendor/base.css'); 11 | app.import('vendor/index.css'); 12 | // Use `app.import` to add additional libraries to the generated 13 | // output files. 14 | // 15 | // If you need to use different assets in different 16 | // environments, specify an object as the first parameter. That 17 | // object's keys should be the environment name and the values 18 | // should be the asset to use in that environment. 19 | // 20 | // If the library that you are including contains AMD or ES6 21 | // modules that you would like to import into your application 22 | // please specify an object with the list of modules as keys 23 | // along with the exports of each module as its value. 24 | 25 | return app.toTree(); 26 | }; 27 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todomvc", 3 | "version": "0.0.0", 4 | "description": "Small description for todomvc goes here", 5 | "private": true, 6 | "directories": { 7 | "doc": "doc", 8 | "test": "tests" 9 | }, 10 | "scripts": { 11 | "build": "ember build", 12 | "start": "ember server", 13 | "test": "ember test", 14 | "make": "bower install && ember build -prod" 15 | }, 16 | "repository": "", 17 | "engines": { 18 | "node": ">= 0.10.0" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "bower": "*", 24 | "broccoli-asset-rev": "^2.4.2", 25 | "ember-ajax": "^2.0.1", 26 | "ember-cli": "2.6.3", 27 | "ember-cli-app-version": "^1.0.0", 28 | "ember-cli-babel": "^5.1.6", 29 | "ember-cli-dependency-checker": "^1.2.0", 30 | "ember-cli-htmlbars": "^1.0.3", 31 | "ember-cli-htmlbars-inline-precompile": "^0.3.1", 32 | "ember-cli-inject-live-reload": "^1.4.0", 33 | "ember-cli-jshint": "^1.0.0", 34 | "ember-cli-qunit": "^1.4.0", 35 | "ember-cli-release": "^0.2.9", 36 | "ember-cli-sri": "^2.1.0", 37 | "ember-cli-uglify": "^1.2.0", 38 | "ember-export-application-global": "^1.0.5", 39 | "ember-inflector": "1.9.4", 40 | "ember-load-initializers": "^0.5.1", 41 | "ember-resolver": "^2.0.3", 42 | "loader.js": "^4.0.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/testem.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true*/ 2 | module.exports = { 3 | framework: 'qunit', 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: [ 7 | 'PhantomJS' 8 | ], 9 | launch_in_dev: [ 10 | 'PhantomJS', 11 | 'Chrome' 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/vendor/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /implementations/ember-2.6.3/vendor/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-font-smoothing: antialiased; 21 | font-smoothing: antialiased; 22 | } 23 | 24 | body { 25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 26 | line-height: 1.4em; 27 | background: #f5f5f5; 28 | color: #4d4d4d; 29 | min-width: 230px; 30 | max-width: 550px; 31 | margin: 0 auto; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-font-smoothing: antialiased; 34 | font-smoothing: antialiased; 35 | font-weight: 300; 36 | } 37 | 38 | button, 39 | input[type="checkbox"] { 40 | outline: none; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | } 46 | 47 | #todoapp { 48 | background: #fff; 49 | margin: 130px 0 40px 0; 50 | position: relative; 51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 53 | } 54 | 55 | #todoapp input::-webkit-input-placeholder { 56 | font-style: italic; 57 | font-weight: 300; 58 | color: #e6e6e6; 59 | } 60 | 61 | #todoapp input::-moz-placeholder { 62 | font-style: italic; 63 | font-weight: 300; 64 | color: #e6e6e6; 65 | } 66 | 67 | #todoapp input::input-placeholder { 68 | font-style: italic; 69 | font-weight: 300; 70 | color: #e6e6e6; 71 | } 72 | 73 | #todoapp h1 { 74 | position: absolute; 75 | top: -155px; 76 | width: 100%; 77 | font-size: 100px; 78 | font-weight: 100; 79 | text-align: center; 80 | color: rgba(175, 47, 47, 0.15); 81 | -webkit-text-rendering: optimizeLegibility; 82 | -moz-text-rendering: optimizeLegibility; 83 | text-rendering: optimizeLegibility; 84 | } 85 | 86 | #new-todo, 87 | .edit { 88 | position: relative; 89 | margin: 0; 90 | width: 100%; 91 | font-size: 24px; 92 | font-family: inherit; 93 | font-weight: inherit; 94 | line-height: 1.4em; 95 | border: 0; 96 | outline: none; 97 | color: inherit; 98 | padding: 6px; 99 | border: 1px solid #999; 100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 101 | box-sizing: border-box; 102 | -webkit-font-smoothing: antialiased; 103 | -moz-font-smoothing: antialiased; 104 | font-smoothing: antialiased; 105 | } 106 | 107 | #new-todo { 108 | padding: 16px 16px 16px 60px; 109 | border: none; 110 | background: rgba(0, 0, 0, 0.003); 111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 112 | } 113 | 114 | #main { 115 | position: relative; 116 | z-index: 2; 117 | border-top: 1px solid #e6e6e6; 118 | } 119 | 120 | label[for='toggle-all'] { 121 | display: none; 122 | } 123 | 124 | #toggle-all { 125 | position: absolute; 126 | top: -55px; 127 | left: -12px; 128 | width: 60px; 129 | height: 34px; 130 | text-align: center; 131 | border: none; /* Mobile Safari */ 132 | } 133 | 134 | #toggle-all:before { 135 | content: '❯'; 136 | font-size: 22px; 137 | color: #e6e6e6; 138 | padding: 10px 27px 10px 27px; 139 | } 140 | 141 | #toggle-all:checked:before { 142 | color: #737373; 143 | } 144 | 145 | #todo-list { 146 | margin: 0; 147 | padding: 0; 148 | list-style: none; 149 | } 150 | 151 | #todo-list li { 152 | position: relative; 153 | font-size: 24px; 154 | border-bottom: 1px solid #ededed; 155 | } 156 | 157 | #todo-list li:last-child { 158 | border-bottom: none; 159 | } 160 | 161 | #todo-list li.editing { 162 | border-bottom: none; 163 | padding: 0; 164 | } 165 | 166 | #todo-list li.editing .edit { 167 | display: block; 168 | width: 506px; 169 | padding: 13px 17px 12px 17px; 170 | margin: 0 0 0 43px; 171 | } 172 | 173 | #todo-list li.editing .view { 174 | display: none; 175 | } 176 | 177 | #todo-list li .toggle { 178 | text-align: center; 179 | width: 40px; 180 | /* auto, since non-WebKit browsers doesn't support input styling */ 181 | height: auto; 182 | position: absolute; 183 | top: 0; 184 | bottom: 0; 185 | margin: auto 0; 186 | border: none; /* Mobile Safari */ 187 | -webkit-appearance: none; 188 | appearance: none; 189 | } 190 | 191 | #todo-list li .toggle:after { 192 | content: url('data:image/svg+xml;utf8,'); 193 | } 194 | 195 | #todo-list li .toggle:checked:after { 196 | content: url('data:image/svg+xml;utf8,'); 197 | } 198 | 199 | #todo-list li label { 200 | white-space: pre; 201 | word-break: break-word; 202 | padding: 15px 60px 15px 15px; 203 | margin-left: 45px; 204 | display: block; 205 | line-height: 1.2; 206 | transition: color 0.4s; 207 | } 208 | 209 | #todo-list li.completed label { 210 | color: #d9d9d9; 211 | text-decoration: line-through; 212 | } 213 | 214 | #todo-list li .destroy { 215 | display: none; 216 | position: absolute; 217 | top: 0; 218 | right: 10px; 219 | bottom: 0; 220 | width: 40px; 221 | height: 40px; 222 | margin: auto 0; 223 | font-size: 30px; 224 | color: #cc9a9a; 225 | margin-bottom: 11px; 226 | transition: color 0.2s ease-out; 227 | } 228 | 229 | #todo-list li .destroy:hover { 230 | color: #af5b5e; 231 | } 232 | 233 | #todo-list li .destroy:after { 234 | content: '×'; 235 | } 236 | 237 | #todo-list li:hover .destroy { 238 | display: block; 239 | } 240 | 241 | #todo-list li .edit { 242 | display: none; 243 | } 244 | 245 | #todo-list li.editing:last-child { 246 | margin-bottom: -1px; 247 | } 248 | 249 | #footer { 250 | color: #777; 251 | padding: 10px 15px; 252 | height: 20px; 253 | text-align: center; 254 | border-top: 1px solid #e6e6e6; 255 | } 256 | 257 | #footer:before { 258 | content: ''; 259 | position: absolute; 260 | right: 0; 261 | bottom: 0; 262 | left: 0; 263 | height: 50px; 264 | overflow: hidden; 265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 266 | 0 8px 0 -3px #f6f6f6, 267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 268 | 0 16px 0 -6px #f6f6f6, 269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 270 | } 271 | 272 | #todo-count { 273 | float: left; 274 | text-align: left; 275 | } 276 | 277 | #todo-count strong { 278 | font-weight: 300; 279 | } 280 | 281 | #filters { 282 | margin: 0; 283 | padding: 0; 284 | list-style: none; 285 | position: absolute; 286 | right: 0; 287 | left: 0; 288 | } 289 | 290 | #filters li { 291 | display: inline; 292 | } 293 | 294 | #filters li a { 295 | color: inherit; 296 | margin: 3px; 297 | padding: 3px 7px; 298 | text-decoration: none; 299 | border: 1px solid transparent; 300 | border-radius: 3px; 301 | } 302 | 303 | #filters li a.selected, 304 | #filters li a:hover { 305 | border-color: rgba(175, 47, 47, 0.1); 306 | } 307 | 308 | #filters li a.selected { 309 | border-color: rgba(175, 47, 47, 0.2); 310 | } 311 | 312 | #clear-completed, 313 | html #clear-completed:active { 314 | float: right; 315 | position: relative; 316 | line-height: 20px; 317 | text-decoration: none; 318 | cursor: pointer; 319 | position: relative; 320 | } 321 | 322 | #clear-completed:hover { 323 | text-decoration: underline; 324 | } 325 | 326 | #info { 327 | margin: 65px auto 0; 328 | color: #bfbfbf; 329 | font-size: 10px; 330 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 331 | text-align: center; 332 | } 333 | 334 | #info p { 335 | line-height: 1; 336 | } 337 | 338 | #info a { 339 | color: inherit; 340 | text-decoration: none; 341 | font-weight: 400; 342 | } 343 | 344 | #info a:hover { 345 | text-decoration: underline; 346 | } 347 | 348 | /* 349 | Hack to remove background from Mobile Safari. 350 | Can't use it globally since it destroys checkboxes in Firefox 351 | */ 352 | @media screen and (-webkit-min-device-pixel-ratio:0) { 353 | #toggle-all, 354 | #todo-list li .toggle { 355 | background: none; 356 | } 357 | 358 | #todo-list li .toggle { 359 | height: 40px; 360 | } 361 | 362 | #toggle-all { 363 | -webkit-transform: rotate(90deg); 364 | transform: rotate(90deg); 365 | -webkit-appearance: none; 366 | appearance: none; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | #footer { 372 | height: 50px; 373 | } 374 | 375 | #filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/build.min.js: -------------------------------------------------------------------------------- 1 | var app=app||{};(function(){"use strict";app.ALL_TODOS="all";app.ACTIVE_TODOS="active";app.COMPLETED_TODOS="completed";app.Utils={uuid:function(){var i,random;var uuid="";for(i=0;i<32;i++){random=Math.random()*16|0;if(i===8||i===12||i===16||i===20){uuid+="-"}uuid+=(i===12?4:i===16?random&3|8:random).toString(16)}return uuid},pluralize:function(count,word){return count===1?word:word+"s"},extend:function(){var newObj={};for(var i=0;i0){clearButton=React.createElement("button",{className:"clear-completed",onClick:this.props.onClearCompleted},"Clear completed")}var nowShowing=this.props.nowShowing;return React.createElement("footer",{className:"footer"},React.createElement("span",{className:"todo-count"},React.createElement("strong",null,this.props.count)," ",activeTodoWord," left"),React.createElement("ul",{className:"filters"},React.createElement("li",null,React.createElement("a",{href:"#/",className:classNames({selected:nowShowing===app.ALL_TODOS})},"All"))," ",React.createElement("li",null,React.createElement("a",{href:"#/active",className:classNames({selected:nowShowing===app.ACTIVE_TODOS})},"Active"))," ",React.createElement("li",null,React.createElement("a",{href:"#/completed",className:classNames({selected:nowShowing===app.COMPLETED_TODOS})},"Completed"))),clearButton)}});var ESCAPE_KEY=27;var ENTER_KEY=13;var TodoItem=React.createClass({handleSubmit:function(event){var val=this.state.editText.trim();if(val){this.props.onSave(val);this.setState({editText:val})}else{this.props.onDestroy()}},handleEdit:function(){this.props.onEdit();this.setState({editText:this.props.todo.title})},handleKeyDown:function(event){if(event.which===ESCAPE_KEY){this.setState({editText:this.props.todo.title});this.props.onCancel(event)}else if(event.which===ENTER_KEY){this.handleSubmit(event)}},handleChange:function(event){if(this.props.editing){this.setState({editText:event.target.value})}},getInitialState:function(){return{editText:this.props.todo.title}},shouldComponentUpdate:function(nextProps,nextState){return nextProps.todo!==this.props.todo||nextProps.editing!==this.props.editing||nextState.editText!==this.state.editText},componentDidUpdate:function(prevProps){if(!prevProps.editing&&this.props.editing){var node=React.findDOMNode(this.refs.editField);node.focus();node.setSelectionRange(node.value.length,node.value.length)}},render:function(){return React.createElement("li",{className:classNames({completed:this.props.todo.completed,editing:this.props.editing})},React.createElement("div",{className:"view"},React.createElement("input",{className:"toggle",type:"checkbox",checked:this.props.todo.completed,onChange:this.props.onToggle}),React.createElement("label",{onDoubleClick:this.handleEdit},this.props.todo.title),React.createElement("button",{className:"destroy",onClick:this.props.onDestroy})),React.createElement("input",{ref:"editField",className:"edit",value:this.state.editText,onBlur:this.handleSubmit,onChange:this.handleChange,onKeyDown:this.handleKeyDown}))}});var ENTER_KEY=13;var TodoApp=React.createClass({getInitialState:function(){return{nowShowing:app.ALL_TODOS,editing:null,newTodo:""}},componentDidMount:function(){var setState=this.setState;var router=Router({"/":setState.bind(this,{nowShowing:app.ALL_TODOS}),"/active":setState.bind(this,{nowShowing:app.ACTIVE_TODOS}),"/completed":setState.bind(this,{nowShowing:app.COMPLETED_TODOS})});router.init("/")},handleChange:function(event){this.setState({newTodo:event.target.value})},handleNewTodoKeyDown:function(event){if(event.keyCode!==ENTER_KEY){return}event.preventDefault();var val=this.state.newTodo.trim();if(val){this.props.model.addTodo(val);this.setState({newTodo:""})}},toggleAll:function(event){var checked=event.target.checked;this.props.model.toggleAll(checked)},toggle:function(todoToToggle){this.props.model.toggle(todoToToggle)},destroy:function(todo){this.props.model.destroy(todo)},edit:function(todo){this.setState({editing:todo.id})},save:function(todoToSave,text){this.props.model.save(todoToSave,text);this.setState({editing:null})},cancel:function(){this.setState({editing:null})},clearCompleted:function(){this.props.model.clearCompleted()},render:function(){var footer;var main;var todos=this.props.model.todos;var shownTodos=todos.filter(function(todo){switch(this.state.nowShowing){case app.ACTIVE_TODOS:return!todo.completed;case app.COMPLETED_TODOS:return todo.completed;default:return true}},this);var todoItems=shownTodos.map(function(todo){return React.createElement(TodoItem,{key:todo.id,todo:todo,onToggle:this.toggle.bind(this,todo),onDestroy:this.destroy.bind(this,todo),onEdit:this.edit.bind(this,todo),editing:this.state.editing===todo.id,onSave:this.save.bind(this,todo),onCancel:this.cancel})},this);var activeTodoCount=todos.reduce(function(accum,todo){return todo.completed?accum:accum+1},0);var completedCount=todos.length-activeTodoCount;if(activeTodoCount||completedCount){footer=React.createElement(TodoFooter,{count:activeTodoCount,completedCount:completedCount,nowShowing:this.state.nowShowing,onClearCompleted:this.clearCompleted})}if(todos.length){main=React.createElement("section",{className:"main"},React.createElement("input",{className:"toggle-all",type:"checkbox",onChange:this.toggleAll,checked:activeTodoCount===0}),React.createElement("ul",{className:"todo-list"},todoItems))}return React.createElement("div",null,React.createElement("header",{className:"header"},React.createElement("h1",null,"todos"),React.createElement("input",{className:"new-todo",placeholder:"What needs to be done?",value:this.state.newTodo,onKeyDown:this.handleNewTodoKeyDown,onChange:this.handleChange,autoFocus:true})),main,footer)}});var model=new app.TodoModel("react-todos");function render(){ReactDOM.render(React.createElement(TodoApp,{model:model}),document.getElementsByClassName("todoapp")[0])}model.subscribe(render);render()})(); -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React • TodoMVC 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Double-click to edit a todo

13 |

Created by petehunt

14 |

Part of TodoMVC

15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/js/app.jsx: -------------------------------------------------------------------------------- 1 | /*jshint quotmark:false */ 2 | /*jshint white:false */ 3 | /*jshint trailing:false */ 4 | /*jshint newcap:false */ 5 | /*global React, Router*/ 6 | var app = app || {}; 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | app.ALL_TODOS = 'all'; 12 | app.ACTIVE_TODOS = 'active'; 13 | app.COMPLETED_TODOS = 'completed'; 14 | 15 | app.Utils = { 16 | uuid: function () { 17 | /*jshint bitwise:false */ 18 | var i, random; 19 | var uuid = ''; 20 | 21 | for (i = 0; i < 32; i++) { 22 | random = Math.random() * 16 | 0; 23 | if (i === 8 || i === 12 || i === 16 || i === 20) { 24 | uuid += '-'; 25 | } 26 | uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) 27 | .toString(16); 28 | } 29 | 30 | return uuid; 31 | }, 32 | 33 | pluralize: function (count, word) { 34 | return count === 1 ? word : word + 's'; 35 | }, 36 | 37 | extend: function () { 38 | var newObj = {}; 39 | for (var i = 0; i < arguments.length; i++) { 40 | var obj = arguments[i]; 41 | for (var key in obj) { 42 | if (obj.hasOwnProperty(key)) { 43 | newObj[key] = obj[key]; 44 | } 45 | } 46 | } 47 | return newObj; 48 | } 49 | }; 50 | 51 | var Utils = app.Utils; 52 | // Generic "model" object. You can use whatever 53 | // framework you want. For this application it 54 | // may not even be worth separating this logic 55 | // out, but we do this to demonstrate one way to 56 | // separate out parts of your application. 57 | app.TodoModel = function (key) { 58 | this.key = key; 59 | this.todos = []; 60 | this.onChanges = []; 61 | }; 62 | 63 | app.TodoModel.prototype.subscribe = function (onChange) { 64 | this.onChanges.push(onChange); 65 | }; 66 | 67 | app.TodoModel.prototype.inform = function () { 68 | this.onChanges.forEach(function (cb) { cb(); }); 69 | }; 70 | 71 | app.TodoModel.prototype.addTodo = function (title) { 72 | this.todos = this.todos.concat({ 73 | id: Utils.uuid(), 74 | title: title, 75 | completed: false 76 | }); 77 | 78 | this.inform(); 79 | }; 80 | 81 | app.TodoModel.prototype.toggleAll = function (checked) { 82 | // Note: it's usually better to use immutable data structures since they're 83 | // easier to reason about and React works very well with them. That's why 84 | // we use map() and filter() everywhere instead of mutating the array or 85 | // todo items themselves. 86 | this.todos = this.todos.map(function (todo) { 87 | return Utils.extend({}, todo, {completed: checked}); 88 | }); 89 | 90 | this.inform(); 91 | }; 92 | 93 | app.TodoModel.prototype.toggle = function (todoToToggle) { 94 | this.todos = this.todos.map(function (todo) { 95 | return todo !== todoToToggle ? 96 | todo : 97 | Utils.extend({}, todo, {completed: !todo.completed}); 98 | }); 99 | 100 | this.inform(); 101 | }; 102 | 103 | app.TodoModel.prototype.destroy = function (todo) { 104 | this.todos = this.todos.filter(function (candidate) { 105 | return candidate !== todo; 106 | }); 107 | 108 | this.inform(); 109 | }; 110 | 111 | app.TodoModel.prototype.save = function (todoToSave, text) { 112 | this.todos = this.todos.map(function (todo) { 113 | return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text}); 114 | }); 115 | 116 | this.inform(); 117 | }; 118 | 119 | app.TodoModel.prototype.clearCompleted = function () { 120 | this.todos = this.todos.filter(function (todo) { 121 | return !todo.completed; 122 | }); 123 | 124 | this.inform(); 125 | }; 126 | 127 | 128 | var TodoFooter = React.createClass({ 129 | render: function () { 130 | var activeTodoWord = app.Utils.pluralize(this.props.count, 'item'); 131 | var clearButton = null; 132 | 133 | if (this.props.completedCount > 0) { 134 | clearButton = ( 135 | 140 | ); 141 | } 142 | 143 | var nowShowing = this.props.nowShowing; 144 | return ( 145 | 176 | ); 177 | } 178 | }); 179 | 180 | var ESCAPE_KEY = 27; 181 | var ENTER_KEY = 13; 182 | 183 | var TodoItem = React.createClass({ 184 | handleSubmit: function (event) { 185 | var val = this.state.editText.trim(); 186 | if (val) { 187 | this.props.onSave(val); 188 | this.setState({editText: val}); 189 | } else { 190 | this.props.onDestroy(); 191 | } 192 | }, 193 | 194 | handleEdit: function () { 195 | this.props.onEdit(); 196 | this.setState({editText: this.props.todo.title}); 197 | }, 198 | 199 | handleKeyDown: function (event) { 200 | if (event.which === ESCAPE_KEY) { 201 | this.setState({editText: this.props.todo.title}); 202 | this.props.onCancel(event); 203 | } else if (event.which === ENTER_KEY) { 204 | this.handleSubmit(event); 205 | } 206 | }, 207 | 208 | handleChange: function (event) { 209 | if (this.props.editing) { 210 | this.setState({editText: event.target.value}); 211 | } 212 | }, 213 | 214 | getInitialState: function () { 215 | return {editText: this.props.todo.title}; 216 | }, 217 | 218 | /** 219 | * This is a completely optional performance enhancement that you can 220 | * implement on any React component. If you were to delete this method 221 | * the app would still work correctly (and still be very performant!), we 222 | * just use it as an example of how little code it takes to get an order 223 | * of magnitude performance improvement. 224 | */ 225 | shouldComponentUpdate: function (nextProps, nextState) { 226 | return ( 227 | nextProps.todo !== this.props.todo || 228 | nextProps.editing !== this.props.editing || 229 | nextState.editText !== this.state.editText 230 | ); 231 | }, 232 | 233 | /** 234 | * Safely manipulate the DOM after updating the state when invoking 235 | * `this.props.onEdit()` in the `handleEdit` method above. 236 | * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate 237 | * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate 238 | */ 239 | componentDidUpdate: function (prevProps) { 240 | if (!prevProps.editing && this.props.editing) { 241 | var node = React.findDOMNode(this.refs.editField); 242 | node.focus(); 243 | node.setSelectionRange(node.value.length, node.value.length); 244 | } 245 | }, 246 | 247 | render: function () { 248 | return ( 249 |
  • 253 |
    254 | 260 | 263 |
    265 | 273 |
  • 274 | ); 275 | } 276 | }); 277 | 278 | var ENTER_KEY = 13; 279 | 280 | var TodoApp = React.createClass({ 281 | getInitialState: function () { 282 | return { 283 | nowShowing: app.ALL_TODOS, 284 | editing: null, 285 | newTodo: '' 286 | }; 287 | }, 288 | 289 | componentDidMount: function () { 290 | var setState = this.setState; 291 | var router = Router({ 292 | '/': setState.bind(this, {nowShowing: app.ALL_TODOS}), 293 | '/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}), 294 | '/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS}) 295 | }); 296 | router.init('/'); 297 | }, 298 | 299 | handleChange: function (event) { 300 | this.setState({newTodo: event.target.value}); 301 | }, 302 | 303 | handleNewTodoKeyDown: function (event) { 304 | if (event.keyCode !== ENTER_KEY) { 305 | return; 306 | } 307 | 308 | event.preventDefault(); 309 | 310 | var val = this.state.newTodo.trim(); 311 | 312 | if (val) { 313 | this.props.model.addTodo(val); 314 | this.setState({newTodo: ''}); 315 | } 316 | }, 317 | 318 | toggleAll: function (event) { 319 | var checked = event.target.checked; 320 | this.props.model.toggleAll(checked); 321 | }, 322 | 323 | toggle: function (todoToToggle) { 324 | this.props.model.toggle(todoToToggle); 325 | }, 326 | 327 | destroy: function (todo) { 328 | this.props.model.destroy(todo); 329 | }, 330 | 331 | edit: function (todo) { 332 | this.setState({editing: todo.id}); 333 | }, 334 | 335 | save: function (todoToSave, text) { 336 | this.props.model.save(todoToSave, text); 337 | this.setState({editing: null}); 338 | }, 339 | 340 | cancel: function () { 341 | this.setState({editing: null}); 342 | }, 343 | 344 | clearCompleted: function () { 345 | this.props.model.clearCompleted(); 346 | }, 347 | 348 | render: function () { 349 | var footer; 350 | var main; 351 | var todos = this.props.model.todos; 352 | 353 | var shownTodos = todos.filter(function (todo) { 354 | switch (this.state.nowShowing) { 355 | case app.ACTIVE_TODOS: 356 | return !todo.completed; 357 | case app.COMPLETED_TODOS: 358 | return todo.completed; 359 | default: 360 | return true; 361 | } 362 | }, this); 363 | 364 | var todoItems = shownTodos.map(function (todo) { 365 | return ( 366 | 376 | ); 377 | }, this); 378 | 379 | var activeTodoCount = todos.reduce(function (accum, todo) { 380 | return todo.completed ? accum : accum + 1; 381 | }, 0); 382 | 383 | var completedCount = todos.length - activeTodoCount; 384 | 385 | if (activeTodoCount || completedCount) { 386 | footer = 387 | ; 393 | } 394 | 395 | if (todos.length) { 396 | main = ( 397 |
    398 | 404 |
      405 | {todoItems} 406 |
    407 |
    408 | ); 409 | } 410 | 411 | return ( 412 |
    413 |
    414 |

    todos

    415 | 423 |
    424 | {main} 425 | {footer} 426 |
    427 | ); 428 | } 429 | }); 430 | 431 | var model = new app.TodoModel('react-todos'); 432 | 433 | function render() { 434 | ReactDOM.render( 435 | , 436 | document.getElementsByClassName('todoapp')[0] 437 | ); 438 | } 439 | 440 | model.subscribe(render); 441 | render(); 442 | })(); 443 | -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/license.md: -------------------------------------------------------------------------------- 1 | Everything in this repo is MIT License unless otherwise specified. 2 | 3 | Copyright (c) Addy Osmani, Sindre Sorhus, Pascal Hartig, Stephen Sawchuk. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "make": "babel --plugins transform-react-jsx js/app.jsx | uglifyjs -o build.min.js" 5 | }, 6 | "dependencies": { 7 | "classnames": "^2.2.5", 8 | "director": "^1.2.8", 9 | "react": "^15.3.1", 10 | "react-dom": "^15.3.1", 11 | "todomvc-app-css": "^2.0.0", 12 | "todomvc-common": "^1.0.2" 13 | }, 14 | "devDependencies": { 15 | "babel-cli": "^6.10.1", 16 | "babel-plugin-transform-react-jsx": "^6.8.0", 17 | "babel-plugin-uglify": "^1.0.2", 18 | "uglify-js": "^2.6.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /implementations/react-15.3.1-optimized/readme.md: -------------------------------------------------------------------------------- 1 | # React TodoMVC for Benchmarking 2 | 3 | This is an update of the React TodoMVC example to use React 15.1 and 4 | precompiled/minified JSX instead of the slow JSXTransformer. The idea is that 5 | this is a reasonable approximation of the performance characteristics of modern 6 | production React code. 7 | 8 | This is not a perfect reflection of React best practices, because there is no 9 | such thing. If you ask five professional React users what current React best 10 | practices are, you'll get five different answers—each of which will benchmark 11 | differently. React Router, Immutable.js, Redux (with or without sagas), Ramda, 12 | ES2015 (with various Babel plugins), Webpack...the list never ends. 13 | 14 | Rather than wading into that combinatorial explosion, this example is leaving 15 | the original React TodoMVC implementation's supporting stack alone, and only 16 | performing the following upgrades: 17 | 18 | * Upgrade to React 15.1 19 | * Move everything into one JSX file and precompile it with Babel 20 | * Minify with uglify 21 | 22 | If you're curious how it benchmarks with your particular stack, please fork 23 | this repo and find out! 24 | 25 | # Building 26 | 27 | 1. `npm install` 28 | 2. `npm run make` 29 | 3. Open a local server (e.g. with `npm install -g http-server`) and open index.html 30 | 31 | 32 | -------------------------------------------------------------------------------- /implementations/react-15.3.1/build.min.js: -------------------------------------------------------------------------------- 1 | var app=app||{};(function(){"use strict";app.ALL_TODOS="all";app.ACTIVE_TODOS="active";app.COMPLETED_TODOS="completed";app.Utils={uuid:function(){var i,random;var uuid="";for(i=0;i<32;i++){random=Math.random()*16|0;if(i===8||i===12||i===16||i===20){uuid+="-"}uuid+=(i===12?4:i===16?random&3|8:random).toString(16)}return uuid},pluralize:function(count,word){return count===1?word:word+"s"},extend:function(){var newObj={};for(var i=0;i0){clearButton=React.createElement("button",{className:"clear-completed",onClick:this.props.onClearCompleted},"Clear completed")}var nowShowing=this.props.nowShowing;return React.createElement("footer",{className:"footer"},React.createElement("span",{className:"todo-count"},React.createElement("strong",null,this.props.count)," ",activeTodoWord," left"),React.createElement("ul",{className:"filters"},React.createElement("li",null,React.createElement("a",{href:"#/",className:classNames({selected:nowShowing===app.ALL_TODOS})},"All"))," ",React.createElement("li",null,React.createElement("a",{href:"#/active",className:classNames({selected:nowShowing===app.ACTIVE_TODOS})},"Active"))," ",React.createElement("li",null,React.createElement("a",{href:"#/completed",className:classNames({selected:nowShowing===app.COMPLETED_TODOS})},"Completed"))),clearButton)}});var ESCAPE_KEY=27;var ENTER_KEY=13;var TodoItem=React.createClass({handleSubmit:function(event){var val=this.state.editText.trim();if(val){this.props.onSave(val);this.setState({editText:val})}else{this.props.onDestroy()}},handleEdit:function(){this.props.onEdit();this.setState({editText:this.props.todo.title})},handleKeyDown:function(event){if(event.which===ESCAPE_KEY){this.setState({editText:this.props.todo.title});this.props.onCancel(event)}else if(event.which===ENTER_KEY){this.handleSubmit(event)}},handleChange:function(event){if(this.props.editing){this.setState({editText:event.target.value})}},getInitialState:function(){return{editText:this.props.todo.title}},componentDidUpdate:function(prevProps){if(!prevProps.editing&&this.props.editing){var node=React.findDOMNode(this.refs.editField);node.focus();node.setSelectionRange(node.value.length,node.value.length)}},render:function(){return React.createElement("li",{className:classNames({completed:this.props.todo.completed,editing:this.props.editing})},React.createElement("div",{className:"view"},React.createElement("input",{className:"toggle",type:"checkbox",checked:this.props.todo.completed,onChange:this.props.onToggle}),React.createElement("label",{onDoubleClick:this.handleEdit},this.props.todo.title),React.createElement("button",{className:"destroy",onClick:this.props.onDestroy})),React.createElement("input",{ref:"editField",className:"edit",value:this.state.editText,onBlur:this.handleSubmit,onChange:this.handleChange,onKeyDown:this.handleKeyDown}))}});var ENTER_KEY=13;var TodoApp=React.createClass({getInitialState:function(){return{nowShowing:app.ALL_TODOS,editing:null,newTodo:""}},componentDidMount:function(){var setState=this.setState;var router=Router({"/":setState.bind(this,{nowShowing:app.ALL_TODOS}),"/active":setState.bind(this,{nowShowing:app.ACTIVE_TODOS}),"/completed":setState.bind(this,{nowShowing:app.COMPLETED_TODOS})});router.init("/")},handleChange:function(event){this.setState({newTodo:event.target.value})},handleNewTodoKeyDown:function(event){if(event.keyCode!==ENTER_KEY){return}event.preventDefault();var val=this.state.newTodo.trim();if(val){this.props.model.addTodo(val);this.setState({newTodo:""})}},toggleAll:function(event){var checked=event.target.checked;this.props.model.toggleAll(checked)},toggle:function(todoToToggle){this.props.model.toggle(todoToToggle)},destroy:function(todo){this.props.model.destroy(todo)},edit:function(todo){this.setState({editing:todo.id})},save:function(todoToSave,text){this.props.model.save(todoToSave,text);this.setState({editing:null})},cancel:function(){this.setState({editing:null})},clearCompleted:function(){this.props.model.clearCompleted()},render:function(){var footer;var main;var todos=this.props.model.todos;var shownTodos=todos.filter(function(todo){switch(this.state.nowShowing){case app.ACTIVE_TODOS:return!todo.completed;case app.COMPLETED_TODOS:return todo.completed;default:return true}},this);var todoItems=shownTodos.map(function(todo){return React.createElement(TodoItem,{todo:todo,onToggle:this.toggle.bind(this,todo),onDestroy:this.destroy.bind(this,todo),onEdit:this.edit.bind(this,todo),editing:this.state.editing===todo.id,onSave:this.save.bind(this,todo),onCancel:this.cancel})},this);var activeTodoCount=todos.reduce(function(accum,todo){return todo.completed?accum:accum+1},0);var completedCount=todos.length-activeTodoCount;if(activeTodoCount||completedCount){footer=React.createElement(TodoFooter,{count:activeTodoCount,completedCount:completedCount,nowShowing:this.state.nowShowing,onClearCompleted:this.clearCompleted})}if(todos.length){main=React.createElement("section",{className:"main"},React.createElement("input",{className:"toggle-all",type:"checkbox",onChange:this.toggleAll,checked:activeTodoCount===0}),React.createElement("ul",{className:"todo-list"},todoItems))}return React.createElement("div",null,React.createElement("header",{className:"header"},React.createElement("h1",null,"todos"),React.createElement("input",{className:"new-todo",placeholder:"What needs to be done?",value:this.state.newTodo,onKeyDown:this.handleNewTodoKeyDown,onChange:this.handleChange,autoFocus:true})),main,footer)}});var model=new app.TodoModel("react-todos");function render(){ReactDOM.render(React.createElement(TodoApp,{model:model}),document.getElementsByClassName("todoapp")[0])}model.subscribe(render);render()})(); -------------------------------------------------------------------------------- /implementations/react-15.3.1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React • TodoMVC 6 | 7 | 8 | 9 | 10 |
    11 |
    12 |

    Double-click to edit a todo

    13 |

    Created by petehunt

    14 |

    Part of TodoMVC

    15 |
    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /implementations/react-15.3.1/js/app.jsx: -------------------------------------------------------------------------------- 1 | /*jshint quotmark:false */ 2 | /*jshint white:false */ 3 | /*jshint trailing:false */ 4 | /*jshint newcap:false */ 5 | /*global React, Router*/ 6 | var app = app || {}; 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | app.ALL_TODOS = 'all'; 12 | app.ACTIVE_TODOS = 'active'; 13 | app.COMPLETED_TODOS = 'completed'; 14 | 15 | app.Utils = { 16 | uuid: function () { 17 | /*jshint bitwise:false */ 18 | var i, random; 19 | var uuid = ''; 20 | 21 | for (i = 0; i < 32; i++) { 22 | random = Math.random() * 16 | 0; 23 | if (i === 8 || i === 12 || i === 16 || i === 20) { 24 | uuid += '-'; 25 | } 26 | uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) 27 | .toString(16); 28 | } 29 | 30 | return uuid; 31 | }, 32 | 33 | pluralize: function (count, word) { 34 | return count === 1 ? word : word + 's'; 35 | }, 36 | 37 | extend: function () { 38 | var newObj = {}; 39 | for (var i = 0; i < arguments.length; i++) { 40 | var obj = arguments[i]; 41 | for (var key in obj) { 42 | if (obj.hasOwnProperty(key)) { 43 | newObj[key] = obj[key]; 44 | } 45 | } 46 | } 47 | return newObj; 48 | } 49 | }; 50 | 51 | var Utils = app.Utils; 52 | // Generic "model" object. You can use whatever 53 | // framework you want. For this application it 54 | // may not even be worth separating this logic 55 | // out, but we do this to demonstrate one way to 56 | // separate out parts of your application. 57 | app.TodoModel = function (key) { 58 | this.key = key; 59 | this.todos = []; 60 | this.onChanges = []; 61 | }; 62 | 63 | app.TodoModel.prototype.subscribe = function (onChange) { 64 | this.onChanges.push(onChange); 65 | }; 66 | 67 | app.TodoModel.prototype.inform = function () { 68 | this.onChanges.forEach(function (cb) { cb(); }); 69 | }; 70 | 71 | app.TodoModel.prototype.addTodo = function (title) { 72 | this.todos = this.todos.concat({ 73 | id: Utils.uuid(), 74 | title: title, 75 | completed: false 76 | }); 77 | 78 | this.inform(); 79 | }; 80 | 81 | app.TodoModel.prototype.toggleAll = function (checked) { 82 | // Note: it's usually better to use immutable data structures since they're 83 | // easier to reason about and React works very well with them. That's why 84 | // we use map() and filter() everywhere instead of mutating the array or 85 | // todo items themselves. 86 | this.todos = this.todos.map(function (todo) { 87 | return Utils.extend({}, todo, {completed: checked}); 88 | }); 89 | 90 | this.inform(); 91 | }; 92 | 93 | app.TodoModel.prototype.toggle = function (todoToToggle) { 94 | this.todos = this.todos.map(function (todo) { 95 | return todo !== todoToToggle ? 96 | todo : 97 | Utils.extend({}, todo, {completed: !todo.completed}); 98 | }); 99 | 100 | this.inform(); 101 | }; 102 | 103 | app.TodoModel.prototype.destroy = function (todo) { 104 | this.todos = this.todos.filter(function (candidate) { 105 | return candidate !== todo; 106 | }); 107 | 108 | this.inform(); 109 | }; 110 | 111 | app.TodoModel.prototype.save = function (todoToSave, text) { 112 | this.todos = this.todos.map(function (todo) { 113 | return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text}); 114 | }); 115 | 116 | this.inform(); 117 | }; 118 | 119 | app.TodoModel.prototype.clearCompleted = function () { 120 | this.todos = this.todos.filter(function (todo) { 121 | return !todo.completed; 122 | }); 123 | 124 | this.inform(); 125 | }; 126 | 127 | 128 | var TodoFooter = React.createClass({ 129 | render: function () { 130 | var activeTodoWord = app.Utils.pluralize(this.props.count, 'item'); 131 | var clearButton = null; 132 | 133 | if (this.props.completedCount > 0) { 134 | clearButton = ( 135 | 140 | ); 141 | } 142 | 143 | var nowShowing = this.props.nowShowing; 144 | return ( 145 | 176 | ); 177 | } 178 | }); 179 | 180 | var ESCAPE_KEY = 27; 181 | var ENTER_KEY = 13; 182 | 183 | var TodoItem = React.createClass({ 184 | handleSubmit: function (event) { 185 | var val = this.state.editText.trim(); 186 | if (val) { 187 | this.props.onSave(val); 188 | this.setState({editText: val}); 189 | } else { 190 | this.props.onDestroy(); 191 | } 192 | }, 193 | 194 | handleEdit: function () { 195 | this.props.onEdit(); 196 | this.setState({editText: this.props.todo.title}); 197 | }, 198 | 199 | handleKeyDown: function (event) { 200 | if (event.which === ESCAPE_KEY) { 201 | this.setState({editText: this.props.todo.title}); 202 | this.props.onCancel(event); 203 | } else if (event.which === ENTER_KEY) { 204 | this.handleSubmit(event); 205 | } 206 | }, 207 | 208 | handleChange: function (event) { 209 | if (this.props.editing) { 210 | this.setState({editText: event.target.value}); 211 | } 212 | }, 213 | 214 | getInitialState: function () { 215 | return {editText: this.props.todo.title}; 216 | }, 217 | 218 | /** 219 | * Safely manipulate the DOM after updating the state when invoking 220 | * `this.props.onEdit()` in the `handleEdit` method above. 221 | * For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate 222 | * and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate 223 | */ 224 | componentDidUpdate: function (prevProps) { 225 | if (!prevProps.editing && this.props.editing) { 226 | var node = React.findDOMNode(this.refs.editField); 227 | node.focus(); 228 | node.setSelectionRange(node.value.length, node.value.length); 229 | } 230 | }, 231 | 232 | render: function () { 233 | return ( 234 |
  • 238 |
    239 | 245 | 248 |
    250 | 258 |
  • 259 | ); 260 | } 261 | }); 262 | 263 | var ENTER_KEY = 13; 264 | 265 | var TodoApp = React.createClass({ 266 | getInitialState: function () { 267 | return { 268 | nowShowing: app.ALL_TODOS, 269 | editing: null, 270 | newTodo: '' 271 | }; 272 | }, 273 | 274 | componentDidMount: function () { 275 | var setState = this.setState; 276 | var router = Router({ 277 | '/': setState.bind(this, {nowShowing: app.ALL_TODOS}), 278 | '/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}), 279 | '/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS}) 280 | }); 281 | router.init('/'); 282 | }, 283 | 284 | handleChange: function (event) { 285 | this.setState({newTodo: event.target.value}); 286 | }, 287 | 288 | handleNewTodoKeyDown: function (event) { 289 | if (event.keyCode !== ENTER_KEY) { 290 | return; 291 | } 292 | 293 | event.preventDefault(); 294 | 295 | var val = this.state.newTodo.trim(); 296 | 297 | if (val) { 298 | this.props.model.addTodo(val); 299 | this.setState({newTodo: ''}); 300 | } 301 | }, 302 | 303 | toggleAll: function (event) { 304 | var checked = event.target.checked; 305 | this.props.model.toggleAll(checked); 306 | }, 307 | 308 | toggle: function (todoToToggle) { 309 | this.props.model.toggle(todoToToggle); 310 | }, 311 | 312 | destroy: function (todo) { 313 | this.props.model.destroy(todo); 314 | }, 315 | 316 | edit: function (todo) { 317 | this.setState({editing: todo.id}); 318 | }, 319 | 320 | save: function (todoToSave, text) { 321 | this.props.model.save(todoToSave, text); 322 | this.setState({editing: null}); 323 | }, 324 | 325 | cancel: function () { 326 | this.setState({editing: null}); 327 | }, 328 | 329 | clearCompleted: function () { 330 | this.props.model.clearCompleted(); 331 | }, 332 | 333 | render: function () { 334 | var footer; 335 | var main; 336 | var todos = this.props.model.todos; 337 | 338 | var shownTodos = todos.filter(function (todo) { 339 | switch (this.state.nowShowing) { 340 | case app.ACTIVE_TODOS: 341 | return !todo.completed; 342 | case app.COMPLETED_TODOS: 343 | return todo.completed; 344 | default: 345 | return true; 346 | } 347 | }, this); 348 | 349 | var todoItems = shownTodos.map(function (todo) { 350 | return ( 351 | 360 | ); 361 | }, this); 362 | 363 | var activeTodoCount = todos.reduce(function (accum, todo) { 364 | return todo.completed ? accum : accum + 1; 365 | }, 0); 366 | 367 | var completedCount = todos.length - activeTodoCount; 368 | 369 | if (activeTodoCount || completedCount) { 370 | footer = 371 | ; 377 | } 378 | 379 | if (todos.length) { 380 | main = ( 381 |
    382 | 388 |
      389 | {todoItems} 390 |
    391 |
    392 | ); 393 | } 394 | 395 | return ( 396 |
    397 |
    398 |

    todos

    399 | 407 |
    408 | {main} 409 | {footer} 410 |
    411 | ); 412 | } 413 | }); 414 | 415 | var model = new app.TodoModel('react-todos'); 416 | 417 | function render() { 418 | ReactDOM.render( 419 | , 420 | document.getElementsByClassName('todoapp')[0] 421 | ); 422 | } 423 | 424 | model.subscribe(render); 425 | render(); 426 | })(); 427 | -------------------------------------------------------------------------------- /implementations/react-15.3.1/license.md: -------------------------------------------------------------------------------- 1 | Everything in this repo is MIT License unless otherwise specified. 2 | 3 | Copyright (c) Addy Osmani, Sindre Sorhus, Pascal Hartig, Stephen Sawchuk. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /implementations/react-15.3.1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "make": "babel --plugins transform-react-jsx js/app.jsx | uglifyjs -o build.min.js" 5 | }, 6 | "dependencies": { 7 | "classnames": "^2.2.5", 8 | "director": "^1.2.8", 9 | "react": "^15.3.1", 10 | "react-dom": "^15.3.1", 11 | "todomvc-app-css": "^2.0.0", 12 | "todomvc-common": "^1.0.2" 13 | }, 14 | "devDependencies": { 15 | "babel-cli": "^6.10.1", 16 | "babel-plugin-transform-react-jsx": "^6.8.0", 17 | "babel-plugin-uglify": "^1.0.2", 18 | "uglify-js": "^2.6.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /implementations/react-15.3.1/readme.md: -------------------------------------------------------------------------------- 1 | # React TodoMVC for Benchmarking 2 | 3 | This is an update of the React TodoMVC example to use React 15.1 and 4 | precompiled/minified JSX instead of the slow JSXTransformer. The idea is that 5 | this is a reasonable approximation of the performance characteristics of modern 6 | production React code. 7 | 8 | This is not a perfect reflection of React best practices, because there is no 9 | such thing. If you ask five professional React users what current React best 10 | practices are, you'll get five different answers—each of which will benchmark 11 | differently. React Router, Immutable.js, Redux (with or without sagas), Ramda, 12 | ES2015 (with various Babel plugins), Webpack...the list never ends. 13 | 14 | Rather than wading into that combinatorial explosion, this example is leaving 15 | the original React TodoMVC implementation's supporting stack alone, and only 16 | performing the following upgrades: 17 | 18 | * Upgrade to React 15.1 19 | * Move everything into one JSX file and precompile it with Babel 20 | * Minify with uglify 21 | 22 | If you're curious how it benchmarks with your particular stack, please fork 23 | this repo and find out! 24 | 25 | # Building 26 | 27 | 1. `npm install` 28 | 2. `npm run make` 29 | 3. Open a local server (e.g. with `npm install -g http-server`) and open index.html 30 | 31 | 32 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Performance Comparison 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |

    Performance Comparison - TodoMVC

    19 |

    This page lets you test the results of Blazing Fast HTML for yourself. 20 |

    21 |

    Controls are on the right. Pick which implementations you want to race and press run. Try it in different browsers! 22 |

    23 |

    Methodology Notes

    24 |

    To compare different frontend tools, you need to implement something in each one with exactly the same functionality. The TodoMVC project is nice because you often get idiomatic implementations from people close to the various projects. So the code is fair, and the app itself is complex enough that you can do some benchmarking that can reasonably be generalized. Is modifying items in the middle of a list fast? Can the implementation tell the difference between remove-the-first-item and change-99-items-and-remove-the-last-one? Etc. 25 |

    26 |

    Check out this blog post for more information on the methodology we used to make these comparisons as fair as possible. 27 |

    28 |
    29 | 30 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Comparing performance of Elm, React, Ember, and Angular 2 | 3 | This is a benchmark that tries to compare the performance of Elm, React, Ember, and Angular in a fair way. [I highly recommend reading the full analysis][blog], but the very short summary is that Elm is the fastest. 4 | 5 | [blog]: http://elm-lang.org/blog/blazing-fast-html-round-two 6 | 7 | ![Performance Comparison][graph] 8 | 9 | [graph]: graphs/chrome.png 10 | 11 | I collected graphs for a bunch of different browsers [here](/graphs), so you can see how the numbers vary across different JS virtual machines. 12 | 13 | That is just on my computer though, so I wanted everyone to be able to run this in their own browsers and see the results for themselves. You can do that [here](https://evancz.github.io/react-angular-ember-elm-performance-comparison/). 14 | 15 |
    16 | 17 | ## Methodology 18 | 19 | My goal with these benchmarks was to compare renderer performance in a realistic scenario. This means rendering each frame in full, exactly like you would if a real user was interacting with the TodoMVC app. I acheived this by making two major rules: 20 | 21 | 22 | ### No Batching Events 23 | 24 | To run these benchmarks, I need to simulate user input. The naive way to do that is to generate a ton of events on a `for` loop all at once. That results in the following graph: 25 | 26 | ![Performance comparison with Batched Events][batched-graph] 27 | 28 | [batched-graph]: http://elm-lang.org/assets/blog/virtual-dom-charts/batched.png 29 | 30 | Holy smokes, it looks like Elm is 3x to 8x faster than some of the competitors! But everyone is going way faster in the first place too... What is going on here? 31 | 32 | If you run this benchmark for yourself, you will see that each implementation displays exactly four frames: no entries, one-hundred entries, one-hundred *complete* entries, and then no entries again. By generating all the events in a single `for` loop, we ensure that they all end up in a big contiguous block on JavaScript’s event queue. So when Elm or React finally gets a chance to run, they just churn through all these events. JavaScript is single threaded, so while this work is happening, we are unable to refresh the view for the user. So the browser is just not painting any frames at all. Instead of painting 100 frames, we paint just one! 33 | 34 | So this is bad for an obvious reason: it is impossible to make the event queue look like this in practice. When a user creates an event, Elm or React or Angular will *always* get woken up before another user event comes in. A user simply cannot generate a contiguous block of events on the event queue. 35 | 36 | So this is not measuring reality, but it still has some interesting information. **This graph measures the core virtual DOM implementation more directly.** This approach seems to strip out all the time spent by the browser turning DOM into pixels. Turns out, this accounts for an overwhelming majority of the time in the fair graphs. If these numbers are even vaguely decent estimates for time spent in the virtual DOM implementation, Elm is doing really well! 37 | 38 | > **Note:** In the fair approach, we generate a step for every event, [like this][fair], and measure each step individually. In the batched approach, there are only [three steps][batched1] that generate all the events at once, [like this][batched2]. 39 | > 40 | > Also, I left Angular 2 out of this graph because they are doing some trick I do not fully understand. Instead of showing 4 frames, they only show one: no entries. I suspect they are using `setTimeout` or something to delay rendering, and in this particular case, that can mean you only diff the first state against the last state. It just so happens that those are empty todo lists, so you essentially build nothing and diff nothing. I would need to know a lot more about their implementation to sort out exactly what is going on there. 41 | 42 | [fair]: https://github.com/evancz/ui-perf/blob/master/src/add-complete-delete.js#L18-L36 43 | [batched1]: https://github.com/evancz/ui-perf/blob/master/src/add-complete-delete-batched.js#L18-L31 44 | [batched2]: https://github.com/evancz/ui-perf/blob/master/src/add-complete-delete-batched.js#L40-L51 45 | 46 | 47 | ### No `requestAnimationFrame` in Elm 48 | 49 | Browsers typically repaint their content at most 60 times per second. So if you are writing JavaScript that has it changing the content 120 times per second, half of that work is wasted. Furthermore, you want your repaints to be at very even intervals so that the 60 repaints align with the 60hz of most monitors. To get the smoothest animations possible, you need to sync up with this. 50 | 51 | So `requestAnimationFrame` was introduced. It lets you say “here is a function that modifies the DOM, but I want the browser to make the changes whenever *it* thinks it is a good idea.” That means the repaints get smoothed out to 60 FPS no matter how crazy your JS happens to be. 52 | 53 | Say you get events coming in extremely quickly, and you get four events within a single frame. A naive use of `requestAnimationFrame` would just schedule them all to happen in sequence. So you would build all four virtual DOMs, diff them against each other in sequence, and show the final result to the user. We can do better though! We can just skip three of those frames entirely. Instead diff the current virtual DOM against the latest virtual DOM. The end result is exactly the same (the user sees the final result) but we skipped 75% of the work! 54 | 55 | Elm does this optimization by default. If you are using Elm, you already have this enabled and your animations are just way smoother out of the box. None of the other frameworks (Angular, Ember, or React) do this by default, and it is not their fault. If you are writing code in JavaScript (or TypeScript) this optimization is not safe at all. This optimization is all about rescheduling and skipping work, and in JavaScript, that work may have some observable effect on the rest of your program. Say you mutate your some state from the `view`. Simple. Common. There are two ways this can go wrong: 56 | 57 | 1. Using `requestAnimationFrame` means this mutation happens *later* than you expected. In the meantime, you may need to do other work that depends on that mutation having already happened. So if another event comes in *before* `requestAnimationFrame` you now have a very sneaky timing bug. 58 | 59 | 2. If you have `requestAnimationFrame` skip frames, the mutation may just *never* happen. Your application state just does not get updated correctly. This kind of bug would be truly awful to hunt down. You need a specific sequence of events to come in, one of them causing a mutation. You need them to come in so fast that they all happen within a single frame. You also need them to come in a specific order such that the event that causes mutation is one of the ones that gets dropped. This could be the definition of a [Heisenbug](https://en.wikipedia.org/wiki/Heisenbug) and I do not think I could create a more difficult bug on purpose. 60 | 61 | In both cases, the fundamental problem is that mutation is possible in your `view` code in JavaScript or TypeScript. A programmer *can* mutate something, and nothing that the React or Angular team does will change this fact. Given that, I personally think it would be crazy for them to have this kind of optimization turned on by default. In that world, using React or Angular would almost guarantee that you see these bugs in practice. 62 | 63 | To bring it back to these benchmarks, the simulated user input comes in really fast, so if I let Elm use `requestAnimationFrame` like it normally would, it would end up skipping tons of frames. That would look good, but if those events were created by a real human being, I doubt *any* of them would happen within a single frame. So in the more realistic scenario, this optimization is not going to have an impact on events that are as slow as human beings. So yes, it is really nice that Elm has this by default, and it definitely makes sense to take that into account when deciding if you want to use Elm, but it would not be fair for this benchmark. 64 | 65 | 66 | ## Building it Yourself 67 | 68 | If you want to fork this repo and try things out, the easiest way is probably to just switch to the `gh-pages` branch where the necessary assets are already checked in. Otherwise, you need to run something like this: 69 | 70 | ```bash 71 | cd src 72 | elm-make Picker.elm --output=picker.js 73 | ``` 74 | 75 | And then navigate into `implementations/*/readme.md` and follow the build instructions for the various projects. 76 | -------------------------------------------------------------------------------- /src/Picker.elm: -------------------------------------------------------------------------------- 1 | port module Picker exposing (main) 2 | 3 | import Html exposing (..) 4 | import Html.App as App 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | 8 | 9 | 10 | main = 11 | App.programWithFlags 12 | { init = init 13 | , view = view 14 | , update = update 15 | , subscriptions = subscriptions 16 | } 17 | 18 | 19 | 20 | -- MODEL 21 | 22 | 23 | type alias Model = 24 | { running : Bool 25 | , entries : List Entry 26 | } 27 | 28 | 29 | type alias Entry = 30 | { selected : Bool 31 | , id : Int 32 | , impl : Impl 33 | } 34 | 35 | 36 | type alias Impl = 37 | { name : String 38 | , version : String 39 | , url : String 40 | , optimized : Bool 41 | } 42 | 43 | 44 | init : List Impl -> ( Model, Cmd msg ) 45 | init impls = 46 | { running = False 47 | , entries = List.indexedMap (Entry False) impls 48 | } 49 | ! [] 50 | 51 | 52 | 53 | -- UPDATE 54 | 55 | 56 | type Msg 57 | = Toggle Int 58 | | Start 59 | | End 60 | 61 | 62 | update : Msg -> Model -> ( Model, Cmd msg ) 63 | update msg model = 64 | case msg of 65 | Toggle id -> 66 | { model | entries = toggle id model.entries } 67 | ! [] 68 | 69 | Start -> 70 | { model | running = True } 71 | ! [ startSelected model.entries ] 72 | 73 | End -> 74 | { model | running = False } 75 | ! [] 76 | 77 | 78 | toggle : Int -> List Entry -> List Entry 79 | toggle id entries = 80 | case entries of 81 | [] -> 82 | [] 83 | 84 | entry :: rest -> 85 | if entry.id == id then 86 | { entry | selected = not entry.selected } :: rest 87 | 88 | else 89 | entry :: toggle id rest 90 | 91 | 92 | port start : List Impl -> Cmd msg 93 | 94 | 95 | startSelected : List Entry -> Cmd msg 96 | startSelected entries = 97 | start (List.map .impl (List.filter .selected entries)) 98 | 99 | 100 | 101 | -- SUBSCRIPTIONS 102 | 103 | 104 | port end : (() -> msg) -> Sub msg 105 | 106 | 107 | subscriptions : Model -> Sub Msg 108 | subscriptions model = 109 | end (always End) 110 | 111 | 112 | 113 | -- VIEW 114 | 115 | 116 | view : Model -> Html Msg 117 | view { running, entries } = 118 | div [] 119 | [ ul 120 | (if running then [ style [("color", "#aaa")] ] else []) 121 | (List.map (viewEntry running) entries) 122 | , button 123 | [ style [("width","100%")] 124 | , disabled running 125 | , onClick Start 126 | ] 127 | [ text "Run" ] 128 | ] 129 | 130 | 131 | viewEntry : Bool -> Entry -> Html Msg 132 | viewEntry running { id, selected, impl } = 133 | li 134 | (if running then [ pointer ] else [ pointer, onClick (Toggle id) ]) 135 | [ input [ type' "checkbox", checked selected, disabled running ] [] 136 | , text (" " ++ impl.name ++ " " ++ impl.version) 137 | , span 138 | [ style [("color","#aaa")] 139 | ] 140 | [ text (if impl.optimized then " (optimized)" else "") 141 | ] 142 | ] 143 | 144 | 145 | pointer : Attribute msg 146 | pointer = 147 | style [ ("cursor", "pointer") ] -------------------------------------------------------------------------------- /src/add-complete-delete-batched.js: -------------------------------------------------------------------------------- 1 | 2 | var suite = function() { 3 | 4 | 5 | // FACTS 6 | 7 | function getFacts(doc) 8 | { 9 | var input = doc.getElementsByClassName('new-todo')[0]; 10 | return input ? { doc: doc, input: input } : undefined; 11 | } 12 | 13 | 14 | // STEPS 15 | 16 | function addCompleteDeleteSteps(numItems) 17 | { 18 | return [ 19 | { 20 | name: 'Adding ' + numItems + ' Items', 21 | work: add(numItems) 22 | }, 23 | { 24 | name: 'Completing All Items', 25 | work: clickAll('.toggle') 26 | }, 27 | { 28 | name: 'Deleting All Items', 29 | work: clickAll('.destroy') 30 | } 31 | ]; 32 | } 33 | 34 | function add(numItems) 35 | { 36 | return function(facts) 37 | { 38 | var node = facts.input; 39 | 40 | for (var i = 0; i < numItems; i++) 41 | { 42 | var inputEvent = document.createEvent('Event'); 43 | inputEvent.initEvent('input', true, true); 44 | node.value = 'Do task ' + i; 45 | node.dispatchEvent(inputEvent); 46 | 47 | var keydownEvent = document.createEvent('Event'); 48 | keydownEvent.initEvent('keydown', true, true); 49 | keydownEvent.keyCode = 13; 50 | node.dispatchEvent(keydownEvent); 51 | } 52 | }; 53 | } 54 | 55 | function clickAll(selector) 56 | { 57 | return function(facts) 58 | { 59 | var checkboxes = facts.doc.querySelectorAll(selector); 60 | for (var i = 0; i < checkboxes.length; i++) 61 | { 62 | checkboxes[i].click(); 63 | } 64 | }; 65 | } 66 | 67 | 68 | // SUITE 69 | 70 | return { 71 | getFacts: getFacts, 72 | steps: addCompleteDeleteSteps(100) 73 | }; 74 | 75 | 76 | }(); -------------------------------------------------------------------------------- /src/add-complete-delete.js: -------------------------------------------------------------------------------- 1 | 2 | var suite = function() { 3 | 4 | 5 | // FACTS 6 | 7 | function getFacts(doc) 8 | { 9 | var input = doc.getElementsByClassName('new-todo')[0]; 10 | return input ? { doc: doc, input: input } : undefined; 11 | } 12 | 13 | 14 | // STEPS 15 | 16 | function addCompleteDeleteSteps(numItems) 17 | { 18 | var steps = []; 19 | 20 | for (var i = 0; i < numItems; i++) 21 | { 22 | steps.push({ name: 'Inputing ' + i, work: inputTodo(i) }); 23 | steps.push({ name: 'Entering ' + i, work: pressEnter }); 24 | } 25 | 26 | for (var i = 0; i < numItems; i++) 27 | { 28 | steps.push({ name: 'Checking ' + i, work: click('toggle', i) }); 29 | } 30 | 31 | for (var i = 0; i < numItems; i++) 32 | { 33 | steps.push({ name: 'Removing ' + i, work: click('destroy', 0) }); 34 | } 35 | 36 | return steps; 37 | } 38 | 39 | function inputTodo(number) 40 | { 41 | return function(facts) 42 | { 43 | var node = facts.input; 44 | 45 | var inputEvent = document.createEvent('Event'); 46 | inputEvent.initEvent('input', true, true); 47 | node.value = 'Do task ' + number; 48 | node.dispatchEvent(inputEvent); 49 | }; 50 | } 51 | 52 | function pressEnter(facts) 53 | { 54 | var event = document.createEvent('Event'); 55 | event.initEvent('keydown', true, true); 56 | event.key = 'Enter'; 57 | event.keyCode = 13; 58 | event.which = 13; 59 | facts.input.dispatchEvent(event); 60 | } 61 | 62 | function click(className, index) 63 | { 64 | return function(facts) 65 | { 66 | facts.doc.getElementsByClassName(className)[index].click(); 67 | }; 68 | } 69 | 70 | 71 | // SUITE 72 | 73 | return { 74 | getFacts: getFacts, 75 | steps: addCompleteDeleteSteps(100) 76 | }; 77 | 78 | 79 | }(); -------------------------------------------------------------------------------- /src/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "4.0.3 <= v < 5.0.0", 12 | "elm-lang/html": "1.1.0 <= v < 2.0.0" 13 | }, 14 | "elm-version": "0.17.1 <= v < 0.18.0" 15 | } 16 | -------------------------------------------------------------------------------- /src/runner.js: -------------------------------------------------------------------------------- 1 | 2 | // SETUP 3 | 4 | if (!window.performance || !window.performance.now) 5 | { 6 | throw new Error('These tests use performance.now() which is not supported by your browser.'); 7 | } 8 | 9 | 10 | 11 | // RUNNER 12 | 13 | 14 | function runBenchmarks(impls, suite, callback) 15 | { 16 | var frame = document.getElementById('benchmark-frame'); 17 | var results = document.getElementById('benchmark-results'); 18 | 19 | frame.style.display = 'block'; 20 | results.style.visibility = 'hidden'; 21 | while (results.lastChild) { 22 | results.removeChild(results.lastChild); 23 | } 24 | 25 | runImplementations(impls, suite, 0, function() { 26 | var canvas = document.createElement('canvas'); 27 | results.appendChild(canvas); 28 | updateChart(canvas, impls); 29 | frame.style.display = 'none'; 30 | results.style.visibility = 'visible'; 31 | callback(); 32 | }); 33 | } 34 | 35 | 36 | 37 | // RUN IMPLEMENTATIONS 38 | 39 | 40 | function runImplementations(impls, suite, index, done) 41 | { 42 | var impl = impls[index]; 43 | var frame = document.getElementById('benchmark-frame'); 44 | frame.onload = function() 45 | { 46 | withFacts(0, frame.contentDocument, suite.getFacts, function(facts) 47 | { 48 | runSteps(facts, suite.steps, index, 0, [], function(results) 49 | { 50 | impl.results = results; 51 | impl.time = getTotalTime(results); 52 | console.log( 53 | impl.name + ' ' + impl.version 54 | + (impl.optimized ? ' (optimized)' : '') 55 | + ' = ' + trunc(impl.time) + ' ms' 56 | ); 57 | 58 | ++index; 59 | 60 | return (index < impls.length) 61 | ? runImplementations(impls, suite, index, done) 62 | : done(); 63 | }); 64 | }); 65 | } 66 | 67 | frame.src = impl.url; 68 | } 69 | 70 | 71 | function getTotalTime(results) 72 | { 73 | var total = 0; 74 | for (var i = 0; i < results.length; i++) 75 | { 76 | total += results[i].sync; 77 | total += results[i].async; 78 | } 79 | return total; 80 | } 81 | 82 | 83 | function withFacts(tries, doc, getFacts, callback) 84 | { 85 | if (tries > 5) 86 | { 87 | throw new Error('Could not get facts for this implementation.'); 88 | } 89 | 90 | setTimeout(function() { 91 | var facts = getFacts(doc); 92 | typeof facts === 'undefined' 93 | ? withFacts(tries + 1, doc, getFacts, callback) 94 | : callback(facts); 95 | }, 16 * Math.pow(2, tries)); 96 | } 97 | 98 | 99 | 100 | /* RUN STEPS ***/ 101 | 102 | 103 | function runSteps(facts, steps, implIndex, index, results, done) 104 | { 105 | timedStep(steps[index].work, facts, function(syncTime, asyncTime) 106 | { 107 | results.push({ 108 | name: steps[index].name, 109 | sync: syncTime, 110 | async: asyncTime 111 | }); 112 | 113 | ++index; 114 | 115 | if (index < steps.length) 116 | { 117 | return runSteps(facts, steps, implIndex, index, results, done) 118 | } 119 | 120 | return done(results); 121 | }); 122 | } 123 | 124 | 125 | function trunc(time) 126 | { 127 | return Math.round(time); 128 | } 129 | 130 | 131 | function timedStep(work, facts, callback) 132 | { 133 | // time all synchronous work 134 | var start = performance.now(); 135 | work(facts); 136 | var end = performance.now(); 137 | var syncTime = end - start; 138 | 139 | // time ONE round of asynchronous work 140 | var asyncStart = performance.now(); 141 | setTimeout(function() { 142 | var asyncEnd = performance.now(); 143 | callback(syncTime, asyncEnd - asyncStart); 144 | }, 0); 145 | 146 | // if anyone does more than one round, we do not capture it! 147 | } 148 | 149 | 150 | 151 | /* SETUP WORK LIST *********/ 152 | 153 | 154 | function setupWorklist(suite) 155 | { 156 | var impls = suite.impls; 157 | var steps = suite.steps; 158 | 159 | var workList = document.getElementById('work-list'); 160 | 161 | while (workList.lastChild) 162 | { 163 | workList.removeChild(workList.lastChild); 164 | } 165 | 166 | for (var i = 0; i < impls.length; i++) 167 | { 168 | var impl = document.createElement('li'); 169 | var title = document.createTextNode(impls[i].name); 170 | impl.appendChild(title); 171 | workList.appendChild(impl); 172 | } 173 | 174 | var sidebar = document.getElementById('sidebar'); 175 | sidebar.appendChild(workList); 176 | } 177 | 178 | 179 | 180 | /* DRAW CHARTS *************/ 181 | 182 | 183 | function updateChart(canvas, impls) 184 | { 185 | new Chart(canvas, { 186 | type: 'bar', 187 | data: { 188 | labels: impls.map(toLabel), 189 | datasets: [{ 190 | label: 'ms', 191 | data: impls.map(function(impl) { return trunc(impl.time); }), 192 | backgroundColor: impls.map(toColor) 193 | }] 194 | }, 195 | options: { 196 | defaultFontFamily: 'Source Sans Pro', 197 | title: { 198 | display: true, 199 | text: 'Benchmark Results', 200 | fontSize: 20 201 | }, 202 | legend: { 203 | display: false 204 | }, 205 | scales: { 206 | yAxes: [{ 207 | scaleLabel: { 208 | display: true, 209 | labelString: 'Milliseconds (lower is better)', 210 | fontSize: 16 211 | }, 212 | ticks: { 213 | beginAtZero: true 214 | } 215 | }] 216 | } 217 | } 218 | }); 219 | } 220 | 221 | function toLabel(impl) 222 | { 223 | return impl.name + ' ' + impl.version; 224 | } 225 | 226 | function toColor(impl) 227 | { 228 | return impl.optimized 229 | ? 'rgba(200, 12, 192, 0.5)' 230 | : 'rgba(75, 192, 192, 0.5)'; 231 | } -------------------------------------------------------------------------------- /src/theme.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro|Source+Code+Pro); 2 | 3 | body { 4 | padding: 0; 5 | margin: 0; 6 | background-color: rgb(253, 253, 253); 7 | font-family: 'Source Sans Pro', sans-serif; 8 | } 9 | 10 | #benchmark-frame { 11 | width: 800px; 12 | height: 600px; 13 | border: 1px solid black; 14 | margin: 20px; 15 | display: none; 16 | } 17 | 18 | #sidebar { 19 | top: 20px; 20 | right: 20px; 21 | position: absolute; 22 | padding: 10px; 23 | background-color: rgb(240, 240, 240); 24 | border-radius: 4px; 25 | } 26 | 27 | #sidebar ul { 28 | list-style-type: none; 29 | padding: 2px; 30 | margin: 2px; 31 | } 32 | 33 | #benchmark-results { 34 | width: 800px; 35 | margin: 20px; 36 | border-radius: 4px; 37 | display: block; 38 | } --------------------------------------------------------------------------------