Add todoapp benchmark

This commit is contained in:
Roman Vishnevskii 2017-04-24 16:00:32 +07:00
commit 02e7b40c07
218 changed files with 63168 additions and 0 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,58 @@
Virtual DOM
===========
The virtual dom is in the ``vdom`` module.
The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes
and some extensions that allow for an efficient component system.
These extensions are:
``VNodeKind.int``
The node havs a single integer field.
``VNodeKind.bool``
The node has a single boolean field.
``VNodeKint.vthunk``
The node is a `virtual thunk`:idx:. This means there is a
function attached to it that produces the ``VNode`` structure
on demand.
``VNodeKint.dthunk``
The node is a `DOM thunk`:idx:. This means there is a
function attached to it that produces the ``Node`` DOM structure
on demand.
For convenience of the resulting Nim DSL these tags have enum names
that differ from their HTML equivalent:
================= =======================================================
Enum value HTML
================= =======================================================
``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim)
``italic`` ``i``
``bold`` ``b``
``strikethrough`` ``s``
``underlined`` ``u``
================= =======================================================
A virtual dom node also has a special field set called ``key``, an integer
that can be used as a data model specific key/id. It can be accessed in event
handlers to change the data model. See the todoapp for an example of how to
use it.
**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can
all have the same key value.
Event system
============
Karax does not abstract over the event system the DOM offers much: The same
``Event`` structure is used. Every callback has the
signature ``proc (ev: Event; n: VNode)``.

View file

@ -0,0 +1,42 @@
import karax, karaxdsl, vdom, dom, components
const
Play = 0
Big = 1
Small = 2
Normal = 3
proc paused(n: Node): bool {.importcpp: "#.paused".}
proc play(n: Node) {.importcpp.}
proc pause(n: Node) {.importcpp.}
proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".}
proc mplayer*(id, resource: cstring): VNode {.component.} =
proc handler(ev: Event; n: VNode) =
let myVideo = document.getElementById(id)
case n.key
of Play:
if myVideo.paused:
myVideo.play()
else:
myVideo.pause()
of Big: myVideo.width = 560
of Small: myVideo.width = 320
of Normal: myVideo.width = 420
else: discard
result = buildHtml(tdiv):
button(onclick=handler, key=Play):
text "Play/Pause"
button(onclick=handler, key=Big):
text "Big"
button(onclick=handler, key=Small):
text "Small"
button(onclick=handler, key=Normal):
text "Normal"
br()
br()
video(id=id, width="420"):
source(src=resource, `type`="video/mp4"):
text "Your browser does not support HTML5 video."

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Player app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/playerapp.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
## Example app that shows how to write and embed a custom component.
include karaxprelude
import mediaplayer
const url = "https://www.w3schools.com/html/mov_bbb.mp4"
proc createDom(): VNode =
result = buildHtml(table):
tr:
td:
mplayer("vid1", url)
td:
mplayer("vid2", url)
tr:
td:
mplayer("vid3", url)
td:
mplayer("vid4", url)
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../../src"

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
</body>
</html>

View file

@ -0,0 +1,24 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karaxprelude
import jstrutils, dom
var entries: seq[cstring] = @[]
for i in 1..500:
entries.add(cstring("Entry ") & &i)
proc scrollEvent(ev: Event; n: VNode) =
let d = n.dom
if d != nil and inViewport(d.lastChild):
# "load" more data:
for i in 1..50:
entries.add(cstring("Loaded Entry ") & &i)
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"):
for x in entries:
tdiv:
text x
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../../src"

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/todoapp.js"></script>
</body>
</html>

View file

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

View file

@ -0,0 +1,155 @@
import vdom, karax, karaxdsl, jstrutils, components
type
Filter = enum
all, active, completed
var
selectedEntry = -1
filter: Filter
entriesLen: int
proc getItem(key : cstring ): cstring {.importc: "localStorage.getItem"}
proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"}
proc clearEntries() {.importc: "localStorage.clear"}
proc getEntryCstring(pos: int): cstring =
result = getItem(&pos & cstring"cstring")
if result == cstring"null":
result = nil
proc isCompleted(pos: int): bool =
var value = getItem(cstring($pos & "bool"))
result = value == cstring"true"
proc setEntryCstring(pos: int, value: cstring) =
setItem(cstring($pos & "cstring"), value)
proc markAsCompleted(pos: int, value : bool) =
var val = cstring"true"
if not value:
val = cstring"false"
setItem(cstring($pos & "bool"), val)
proc addEntry(str: cstring, bval : bool) =
setEntryCstring(entriesLen, str)
markAsCompleted(entriesLen, bval)
inc entriesLen
proc updateEntry(pos: int, str: cstring, bval: bool) =
setEntryCstring(pos, str)
markAsCompleted(pos, bval)
proc onTodoEnter(ev: Event; n: VNode) =
addEntry(n.value, false)
n.value = ""
proc removeHandler(ev: Event; n: VNode) =
updateEntry(n.key, cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
selectedEntry = n.key
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
setEntryCstring(n.key, n.value)
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = n.key
markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) =
clearEntries()
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entriesLen:
if isCompleted(i): setEntryCstring(i, nil)
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.component.} =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, key=id)
label(onDblClick=editHandler, key=id):
text d
button(class = "destroy", key=id, onclick=removeHandler)
else:
input(class = "edit", name = "title", key=id,
onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus)
proc makeFooter(entriesCount, completedCount: int): VNode {.component.} =
result = buildHtml(footer(class = "footer")):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
a(class = selected(all), href = "#/"):
text "All"
li:
a(class = selected(active), href = "#/active"):
text "Active"
li:
a(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
proc makeHeader(): VNode {.component.} =
result = buildHtml(header(class = "header")):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus)
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
makeHeader()
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
#for i, d in pairs(entries):
for i in 0..entriesLen-1:
var d0 = getEntryCstring(i)
var d1 = isCompleted(i)
if d0 != nil:
let b = case filter
of all: true
of active: not d1
of completed: d1
if b:
createEntry(i, d0, d1, i == selectedEntry)
inc completedCount, ord(d1)
inc entriesCount
makeFooter(entriesCount, completedCount)
setOnHashChange(proc(hash: cstring) =
if hash == "#/": filter = all
elif hash == "#/completed": filter = completed
elif hash == "#/active": filter = active
)
entriesLen = 0
setRenderer createDom

View file

@ -0,0 +1 @@
--path: "../../src"

View file

@ -0,0 +1,13 @@
type
EntryPoint = proc()
proc dynmain =
echo "dynamically loaded"
var plugins {.importc.}: seq[(string, EntryPoint)]
plugins.add(("dyn", EntryPoint dynmain))

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/example.js"></script>
</body>
</html>

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