├── .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 |
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 |
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 |
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 |
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 |
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 | {{todo.title}}
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 |
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 |
138 | Clear completed
139 |
140 | );
141 | }
142 |
143 | var nowShowing = this.props.nowShowing;
144 | return (
145 |
146 |
147 | {this.props.count} {activeTodoWord} left
148 |
149 |
174 | {clearButton}
175 |
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 |
261 | {this.props.todo.title}
262 |
263 |
264 |
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 |
408 | );
409 | }
410 |
411 | return (
412 |
413 |
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 |
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 |
138 | Clear completed
139 |
140 | );
141 | }
142 |
143 | var nowShowing = this.props.nowShowing;
144 | return (
145 |
146 |
147 | {this.props.count} {activeTodoWord} left
148 |
149 |
174 | {clearButton}
175 |
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 |
246 | {this.props.todo.title}
247 |
248 |
249 |
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 |
392 | );
393 | }
394 |
395 | return (
396 |
397 |
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 | }
--------------------------------------------------------------------------------