complete todo example

This commit is contained in:
Evan You 2013-08-07 02:03:16 -04:00
parent dc04a1af69
commit 7d126127e6
14 changed files with 696 additions and 160 deletions

View File

@ -11,6 +11,8 @@ WIP, playing with data binding
### Data Binding
### Event Handling
### Filters
### Computed Properties

View File

@ -1,3 +1,6 @@
- parse textNodes
- parse textNodes?
- method invoke with arguments
- more directives / filters
- sd-if
- sd-route
- nested properties in scope (kinda hard, maybe later)

View File

@ -1,9 +1,6 @@
{
"name": "seed",
"version": "0.0.1",
"dependencies": {
"component/emitter": "*"
},
"main": "src/main.js",
"scripts": [
"src/main.js",

View File

@ -1,119 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
<script src="../dist/seed.js"></script>
<style type="text/css">
.red {
color: red;
}
.done {
text-decoration: line-through;
}
#app.all .all {
font-weight: bold;
}
#app.remaining .todo.done {
display: none;
}
#app.remaining .remaining {
font-weight: bold;
}
#app.completed .todo:not(.done) {
display: none;
}
#app.completed .completed {
font-weight: bold;
}
</style>
</head>
<body>
<div id="app" sd-controller="Todos" sd-class="filter">
<div>
<input placeholder="What needs to be done?" sd-on="change:addTodo">
</div>
<ul sd-show="todos">
<li class="todo" sd-each="todo:todos" sd-class="done:todo.done">
<input type="checkbox" sd-checked="todo.done" sd-on="change:toggleTodo">
<span sd-text="todo.text"></span>
<a sd-on="click:removeTodo">X</a>
</li>
</ul>
<div id="footer">
Total: <span sd-text="total < todos"></span> |
Remaining: <span sd-text="remaining < completed"></span> |
Completed: <span sd-text="completed"></span>
<br>
<a class="all" sd-on="click:setFilter">Show All</a> |
<a class="remaining" sd-on="click:setFilter">Show Remaining</a> |
<a class="completed" sd-on="click:setFilter">Show Completed</a>
<br>
<a sd-on="click:removeCompleted">Remove Completed</a>
</div>
</div>
<script>
var Seed = require('seed')
var todos = [
{ text: 'make nesting controllers work', done: true },
{ text: 'complete ArrayWatcher', done: false },
{ text: 'computed properties', done: false },
{ text: 'parse textnodes', done: false }
]
Seed.controller('Todos', function (scope) {
// regular properties
scope.todos = todos
scope.filter = 'all'
scope.completed = todos.reduce(function (count, todo) {
return count + (todo.done ? 1 : 0)
}, 0)
// computed properties
scope.total = function () {
return scope.todos.length
}
scope.remaining = function () {
return scope.todos.length - scope.completed
}
// event handlers
scope.addTodo = function (e) {
var val = e.el.value
if (val) {
e.el.value = ''
scope.todos.unshift({ text: val, done: false })
}
}
scope.removeTodo = function (e) {
scope.todos.remove(e.scope)
scope.completed -= e.scope.done ? 1 : 0
}
scope.toggleTodo = function (e) {
scope.completed += e.scope.done ? 1 : -1
}
scope.setFilter = function (e) {
scope.filter = e.el.className
}
scope.removeCompleted = function () {
scope.todos = scope.todos.filter(function (todo) {
return !todo.done
})
}
})
var app = Seed.bootstrap()
</script>
</body>
</html>

80
examples/todos/app.js Normal file
View File

@ -0,0 +1,80 @@
var Seed = require('seed')
var todos = [
{ text: 'make nesting controllers work', done: true },
{ text: 'complete ArrayWatcher', done: false },
{ text: 'computed properties', done: false },
{ text: 'parse textnodes', done: false }
]
Seed.controller('Todos', function (scope) {
// regular properties -----------------------------------------------------
scope.todos = todos
scope.filter = 'all'
scope.allDone = false
scope.remaining = todos.reduce(function (count, todo) {
return count + (todo.done ? 0 : 1)
}, 0)
// computed properties ----------------------------------------------------
scope.total = function () {
return scope.todos.length
}
scope.completed = function () {
return scope.total() - scope.remaining
}
scope.itemLabel = function () {
return scope.remaining > 1 ? 'items' : 'item'
}
// event handlers ---------------------------------------------------------
scope.addTodo = function (e) {
var val = e.el.value
if (val) {
e.el.value = ''
scope.todos.unshift({ text: val, done: false })
}
scope.remaining++
}
scope.removeTodo = function (e) {
scope.todos.remove(e.scope)
scope.remaining -= e.scope.done ? 0 : 1
}
scope.updateCount = function (e) {
scope.remaining += e.scope.done ? -1 : 1
scope.allDone = scope.remaining === 0
}
scope.edit = function (e) {
e.scope.editing = true
}
scope.stopEdit = function (e) {
e.scope.editing = false
}
scope.setFilter = function (e) {
scope.filter = e.el.dataset.filter
}
scope.toggleAll = function (e) {
scope.todos.forEach(function (todo) {
todo.done = e.el.checked
})
scope.remaining = e.el.checked ? 0 : scope.total()
}
scope.removeCompleted = function () {
scope.todos = scope.todos.filter(function (todo) {
return !todo.done
})
}
})
Seed.bootstrap()

BIN
examples/todos/bg.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

13
examples/todos/custom.css Normal file
View File

@ -0,0 +1,13 @@
#todoapp.all [data-filter="all"],
#todoapp.active [data-filter="active"],
#todoapp.completed [data-filter="completed"] {
font-weight: bold;
}
#todoapp.active #todo-list li.completed {
display: none;
}
#todoapp.completed #todo-list li:not(.completed) {
display: none;
}

82
examples/todos/index.html Normal file
View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="todomvc.css">
<link rel="stylesheet" type="text/css" href="custom.css">
</head>
<body>
<section id="todoapp" sd-controller="Todos" sd-class="filter">
<header id="header">
<h1>todos</h1>
<!-- main input box -->
<input
id="new-todo"
autofocus
sd-on="keyup:addTodo | key enter"
placeholder="What needs to be done?"
>
</header>
<section id="main" sd-show="total < todos">
<input id="toggle-all" type="checkbox" sd-checked="allDone" sd-on="change:toggleAll">
<ul id="todo-list">
<!-- a single todo item -->
<li sd-each="todo:todos" sd-class="completed:todo.done, editing:todo.editing">
<div class="view">
<input
class="toggle"
type="checkbox"
sd-checked="todo.done"
sd-on="change:updateCount"
>
<label
sd-text="todo.text"
sd-on="dblclick:edit"
></label>
<button class="destroy" sd-on="click:removeTodo"></button>
</div>
<input
class="edit"
type="text"
sd-focus="todo.editing"
sd-on="blur:stopEdit, keyup:stopEdit | key enter"
sd-value="todo.text"
>
</li>
</ul>
</section>
<!-- footer controls -->
<footer id="footer" sd-show="total < todos">
<span id="todo-count">
<strong sd-text="remaining"></strong>
<span sd-text="itemLabel < remaining"></span>
left
</span>
<ul id="filters">
<li><a href="#/all" data-filter="all" sd-on="click:setFilter">All</a></li>
<li><a href="#/active" data-filter="active" sd-on="click:setFilter">Active</a></li>
<li><a href="#/completed" data-filter="completed" sd-on="click:setFilter">Completed</a></li>
</ul>
<button id="clear-completed" sd-on="click:removeCompleted">
Remove Completed (<span sd-text="completed < total remaining"></span>)
</button>
</footer>
</section>
<!-- info -->
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Powered by <a href="https://github.com/yyx990803/seed">Seed.js</a></p>
<p>Created by <a href="http://evanyou.me">Evan You</a></p>
</footer>
<!-- js -->
<script src="../../dist/seed.js"></script>
<script src="app.js"></script>
</body>
</html>

414
examples/todos/todomvc.css Executable file
View File

@ -0,0 +1,414 @@
html,
body {
margin: 0;
padding: 0;
}
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
color: inherit;
-webkit-appearance: none;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#todoapp {
background: #fff;
background: rgba(255, 255, 255, 0.9);
margin: 130px 0 40px 0;
border: 1px solid #ccc;
position: relative;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2),
0 25px 50px 0 rgba(0, 0, 0, 0.15);
}
#todoapp:before {
content: '';
border-left: 1px solid #f5d6d6;
border-right: 1px solid #f5d6d6;
width: 2px;
position: absolute;
top: 0;
left: 40px;
height: 100%;
}
#todoapp input::-webkit-input-placeholder {
font-style: italic;
}
#todoapp input::-moz-placeholder {
font-style: italic;
color: #a9a9a9;
}
#todoapp h1 {
position: absolute;
top: -120px;
width: 100%;
font-size: 70px;
font-weight: bold;
text-align: center;
color: #b3b3b3;
color: rgba(255, 255, 255, 0.3);
text-shadow: -1px -1px rgba(0, 0, 0, 0.2);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
-o-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#header {
padding-top: 15px;
border-radius: inherit;
}
#header:before {
content: '';
position: absolute;
top: 0;
right: 0;
left: 0;
height: 15px;
z-index: 2;
border-bottom: 1px solid #6c615c;
background: #8d7d77;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8)));
background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8));
filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670');
border-top-left-radius: 1px;
border-top-right-radius: 1px;
}
#new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 24px;
font-family: 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);
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
-o-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
font-smoothing: antialiased;
}
#new-todo {
padding: 16px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.02);
z-index: 2;
box-shadow: none;
}
#main {
position: relative;
z-index: 2;
border-top: 1px dotted #adadad;
}
label[for='toggle-all'] {
display: none;
}
#toggle-all {
position: absolute;
top: -42px;
left: -4px;
width: 40px;
text-align: center;
border: none; /* Mobile Safari */
}
#toggle-all:before {
content: '»';
font-size: 28px;
color: #d9d9d9;
padding: 0 25px 7px;
}
#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 dotted #ccc;
}
#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;
/*-moz-appearance: none;*/
-ms-appearance: none;
-o-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
content: '✔';
line-height: 43px; /* 40 + a couple of pixels visual adjustment */
font-size: 20px;
color: #d9d9d9;
text-shadow: 0 -1px 0 #bfbfbf;
}
#todo-list li .toggle:checked:after {
color: #85ada7;
text-shadow: 0 1px 0 #669991;
bottom: 1px;
position: relative;
}
#todo-list li label {
word-break: break-word;
padding: 15px;
margin-left: 45px;
display: block;
line-height: 1.2;
-webkit-transition: color 0.4s;
-moz-transition: color 0.4s;
-ms-transition: color 0.4s;
-o-transition: color 0.4s;
transition: color 0.4s;
}
#todo-list li.completed label {
color: #a9a9a9;
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: 22px;
color: #a88a8a;
-webkit-transition: all 0.2s;
-moz-transition: all 0.2s;
-ms-transition: all 0.2s;
-o-transition: all 0.2s;
transition: all 0.2s;
}
#todo-list li .destroy:hover {
text-shadow: 0 0 1px #000,
0 0 10px rgba(199, 107, 107, 0.8);
-webkit-transform: scale(1.3);
-moz-transform: scale(1.3);
-ms-transform: scale(1.3);
-o-transform: scale(1.3);
transform: scale(1.3);
}
#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: 0 15px;
position: absolute;
right: 0;
bottom: -31px;
left: 0;
height: 20px;
z-index: 1;
text-align: center;
}
#footer:before {
content: '';
position: absolute;
right: 0;
bottom: 31px;
left: 0;
height: 50px;
z-index: -1;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3),
0 6px 0 -3px rgba(255, 255, 255, 0.8),
0 7px 1px -3px rgba(0, 0, 0, 0.3),
0 43px 0 -6px rgba(255, 255, 255, 0.8),
0 44px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
float: left;
text-align: left;
}
#filters {
margin: 0;
padding: 0;
list-style: none;
position: absolute;
right: 0;
left: 0;
}
#filters li {
display: inline;
}
#filters li a {
color: #83756f;
margin: 2px;
text-decoration: none;
}
#filters li a.selected {
font-weight: bold;
}
#clear-completed {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
background: rgba(0, 0, 0, 0.1);
font-size: 11px;
padding: 0 10px;
border-radius: 3px;
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2);
}
#clear-completed:hover {
background: rgba(0, 0, 0, 0.15);
box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3);
}
#info {
margin: 65px auto 0;
color: #a6a6a6;
font-size: 12px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7);
text-align: center;
}
#info a {
color: inherit;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox and Opera
*/
@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 {
top: -56px;
left: -15px;
width: 65px;
height: 41px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
appearance: none;
}
}
.hidden{
display:none;
}

View File

@ -95,7 +95,12 @@ function Directive (directiveName, expression) {
// called when a dependency has changed
Directive.prototype.refresh = function () {
if (this.value) {
this._update(this.value.call(this.seed.scope))
var value = this.value.call(this.seed.scope)
this._update(
this.filters
? this.applyFilters(value)
: value
)
}
if (this.binding.refreshDependents) {
this.binding.refreshDependents()
@ -104,16 +109,17 @@ Directive.prototype.refresh = function () {
// called when a new value is set
Directive.prototype.update = function (value) {
if (value && (value === this.value)) return
this.value = value
// computed property
if (typeof value === 'function' && !this.fn) {
value = value()
}
// apply filters
if (this.filters) {
value = this.applyFilters(value)
}
this._update(value)
this._update(
this.filters
? this.applyFilters(value)
: value
)
if (this.deps) this.refresh()
}

View File

@ -26,8 +26,11 @@ var mutationHandlers = {
unshift: function (m) {
var self = this
m.args.forEach(function (data, i) {
var seed = self.buildItem(data, i)
self.container.insertBefore(seed.el, self.collection[m.args.length].$seed.el)
var seed = self.buildItem(data, i),
ref = self.collection.length > m.args.length
? self.collection[m.args.length].$seed.el
: self.marker
self.container.insertBefore(seed.el, ref)
})
self.reorder()
},
@ -132,7 +135,6 @@ module.exports = {
},
reorder: function () {
console.log('reorder')
this.collection.forEach(function (scope, i) {
scope.$index = i
})

View File

@ -4,14 +4,23 @@ module.exports = {
each : require('./each'),
text: function (value) {
this.el.textContent = value === null ?
'' : value.toString()
this.el.textContent =
(value !== null && value !== undefined)
? value.toString() : ''
},
show: function (value) {
this.el.style.display = value ? '' : 'none'
},
hide: function (value) {
this.el.style.display = value ? 'none' : ''
},
focus: function (value) {
this.el[value ? 'focus' : 'blur']()
},
class: function (value) {
if (this.arg) {
this.el.classList[value ? 'add' : 'remove'](this.arg)
@ -22,17 +31,32 @@ module.exports = {
}
},
value: {
bind: function () {
var el = this.el, self = this
this.change = function () {
self.seed.scope[self.key] = el.value
}
el.addEventListener('change', this.change)
},
update: function (value) {
this.el.value = value
},
unbind: function () {
this.el.removeEventListener('change', this.change)
}
},
checked: {
bind: function () {
var el = this.el,
self = this
var el = this.el, self = this
this.change = function () {
self.seed.scope[self.key] = el.checked
}
el.addEventListener('change', this.change)
},
update: function (value) {
this.el.checked = value
this.el.checked = !!value
},
unbind: function () {
this.el.removeEventListener('change', this.change)

View File

@ -1,3 +1,13 @@
var keyCodes = {
enter: 13,
tab: 9,
'delete': 46,
up: 38,
left: 37,
right: 39,
down: 40
}
module.exports = {
capitalize: function (value) {
@ -7,6 +17,27 @@ module.exports = {
uppercase: function (value) {
return value.toString().toUpperCase()
},
currency: function (value, args) {
if (!value) return value
var sign = (args && args[0]) || '$',
i = value % 3,
f = '.' + value.toFixed(2).slice(-2),
s = Math.floor(value).toString()
return sign + s.slice(0, i) + s.slice(i).replace(/(\d{3})(?=\d)/g, '$1,') + f
},
key: function (handler, args) {
var code = keyCodes[args[0]]
if (!code) {
code = parseInt(args[0], 10)
}
return function (e) {
if (e.originalEvent.keyCode === code) {
handler(e)
}
}
}
}

View File

@ -1,5 +1,4 @@
var Emitter = require('emitter'),
config = require('./config'),
var config = require('./config'),
DirectiveParser = require('./directive-parser'),
TextNodeParser = require('./textnode-parser')
@ -7,20 +6,6 @@ var slice = Array.prototype.slice,
ctrlAttr = config.prefix + '-controller',
eachAttr = config.prefix + '-each'
function determinScope (key, scope) {
if (key.nesting) {
var levels = key.nesting
while (scope.parentSeed && levels--) {
scope = scope.parentSeed
}
} else if (key.root) {
while (scope.parentSeed) {
scope = scope.parentSeed
}
}
return scope
}
function Seed (el, options) {
if (typeof el === 'string') {
@ -45,11 +30,15 @@ function Seed (el, options) {
|| {}
el.removeAttribute(dataPrefix)
// if the passed in data is already consumed by
// a Seed instance, make a copy from it
if (scope.$seed) {
scope = this.scope = scope.$dump()
}
scope.$seed = this
scope.$destroy = this._destroy.bind(this)
scope.$dump = this._dump.bind(this)
scope.$on = this.on.bind(this)
scope.$emit = this.emit.bind(this)
scope.$index = options.index
scope.$parent = options.parentSeed && options.parentSeed.scope
@ -76,7 +65,7 @@ Seed.prototype._compileNode = function (node, root) {
self._compileTextNode(node)
} else {
} else if (node.nodeType !== 8) { // exclude comment nodes
var eachExp = node.getAttribute(eachAttr),
ctrlExp = node.getAttribute(ctrlAttr)
@ -164,9 +153,7 @@ Seed.prototype._bind = function (node, directive) {
}
// set initial value
if (binding.value) {
directive.update(binding.value)
}
directive.update(binding.value)
// computed properties
if (directive.deps) {
@ -259,6 +246,20 @@ Seed.prototype._dump = function () {
return dump
}
Emitter(Seed.prototype)
// Helpers --------------------------------------------------------------------
function determinScope (key, scope) {
if (key.nesting) {
var levels = key.nesting
while (scope.parentSeed && levels--) {
scope = scope.parentSeed
}
} else if (key.root) {
while (scope.parentSeed) {
scope = scope.parentSeed
}
}
return scope
}
module.exports = Seed