大凡出名的MVC,MVVM框架都有todo例子,我們也搞一下看看avalon是否這么便宜。
我們先從react的todo例子中扒一下HTML與CSS用用。
<!doctype html>
<html lang="en" data-framework="react">
<head>
<meta charset="utf-8">
<title>React • TodoMVC</title>
<link rel="stylesheet" href="bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://github.com/petehunt/">petehunt</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/react/react-with-addons.js"></script>
<script src="bower_components/react/JSXTransformer.js"></script>
<script src="bower_components/director/build/director.js"></script>
<script src="js/utils.js"></script>
<script src="js/todoModel.js"></script>
<!-- jsx is an optional syntactic sugar that transforms methods in React's
`render` into an HTML-looking format. Since the two models above are
unrelated to React, we didn't need those transforms. -->
<script type="text/jsx" src="js/todoItem.jsx"></script>
<script type="text/jsx" src="js/footer.jsx"></script>
<script type="text/jsx" src="js/app.jsx"></script>
</body>
</html>
改成下面這樣
<!doctype html>
<html lang="cn">
<head>
<meta charset="utf-8">
<title>avalon • TodoMVC</title>
<link rel="stylesheet" href="http://todomvc.com/architecture-examples/react/bower_components/todomvc-common/base.css">
</head>
<body>
<section id="todoapp"></section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/RubyLouvre/avalon">司徒正美</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>
由於用了許多新標簽與CSS3,因此肯定在舊式IE下一塌糊塗,大家需要在chrome下瀏覽。

我們添加一些avalon東西,改一下版權什么的
<!doctype html>
<html lang="cn">
<head>
<meta charset="utf-8">
<title>avalon • TodoMVC</title>
<link rel="stylesheet" href="http://todomvc.com/architecture-examples/react/bower_components/todomvc-common/base.css">
<script src="avalon.js"></script>
<style>
.ms-controller{
visibility: hidden;
}
</style>
<script>
var model = avalon.define({
$id: "todo"
})
</script>
</head>
<body ms-controller="todo" class="ms-controller">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<form id="todo-form" autocomplete="off">
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</form>
</header>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/RubyLouvre/avalon">司徒正美</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>

好了,有模有樣了。我們添加一些實際功能了。todoMVC主要是演示往一個數組里添加東西,然后可以編輯刪除它。那么我們就先得有一個變量來保存要添加的東西,及一個可以往里面添加的東西的數組。我把它們命名為newTodo與todos。添加是一個用戶行為,因此我們需要一個addTodo的提交回調。
<!doctype html>
<html lang="cn">
<head>
<meta charset="utf-8">
<title>avalon • TodoMVC</title>
<link rel="stylesheet" href="http://todomvc.com/architecture-examples/react/bower_components/todomvc-common/base.css">
<script src="avalon.js"></script>
<style>
.ms-controller{
visibility: hidden;
}
</style>
<script>
var model = avalon.define({
$id: "todo",
newTodo: "",
todos: [],
addTodo: function(e) {
e.preventDefault()//阻止頁面刷新
var newTodo = model.newTodo.trim()
if (!newTodo.length) {
return
}
model.todos.push({
title: newTodo,
completed: false
});
model.newTodo = ""//清空內容
}
})
</script>
</head>
<body ms-controller="todo" class="ms-controller">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<form id="todo-form" ms-submit="addTodo" autocomplete="off">
<input id="new-todo" ms-duplex="newTodo" placeholder="What needs to be done?" autofocus>
</form>
</header>
<section id="main" ms-visible="todos.size()" >
<input id="toggle-all" type="checkbox" >
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li ms-repeat-todo="todos" ms-class="completed: todo.completed" >
<div class="view">
<input class="toggle" type="checkbox" >
<label >{{todo.title}}</label>
<button class="destroy" ></button>
</div>
<form>
<input class="edit" >
</form>
</li>
</ul>
</section>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/RubyLouvre/avalon">司徒正美</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>

我們看到ms-visible是對應todos.size()而不是todos.length,那是因為數組的原生屬性length無法hack進去,因此實現不了監控功能,實現不了監控也無法雙向綁定了。
我們再來看如何實現編輯刪除。編輯需要一個雙擊事件,並且一個時期內只能有一個todo處於編輯狀態。這里我使用editingIndex屬性,它是保存正在編輯的元素的索引值,如果不想編輯,將它置為NaN就行了。比如那個文本域綁定一個失去焦點事件,那個回調就是這樣干。移除元素,直接使用ms-repeat綁定臨時生成的$remove方法。
editingIndex: NaN,
editTodo: function($index) {
model.editingIndex = $index
//為了用戶體驗,有時不得不寫一些DOM處理
var el = this.parentNode.parentNode
setTimeout(function() {//讓光標定位於文本之后
var input = el.querySelector("input.edit")
input.focus()
input.value = input.value
})
},
doneEditing: function() {//還原
model.editingIndex = NaN
},
<ul id="todo-list">
<li ms-repeat-todo="todos" ms-class="completed: todo.completed" ms-class-1="editing: $index === editingIndex">
<div class="view">
<input class="toggle" type="checkbox" >
<label ms-dblclick="editTodo($index)">{{todo.title}}</label>
<button class="destroy" ms-click="$remove"></button>
</div>
<form>
<input class="edit" ms-duplex="todo.title" ms-blur="doneEditing" >
</form>
</li>
</ul>

接着來做全選非選功能,我們需要一個變量來保存全選狀態,然后監聽它來同步下面UI列表的checkbox的勾選情況,這個使用$watch回調實現就行了。但UI列表的每個元素的complete是否打勾也會影響到上方的全選checkbox,這時不可能為每個元素添加$watch回調,我們改用data-duplex-changed回調checkOne實現。
// 下面幾行在define函數里
allChecked: false,
checkOne: function() {//點擊UI列表的checkbox時
model.$unwatch() //阻止下面allChecked的$watch回調觸發
model.allChecked = model.todos.every(function(val) {
return val.completed
})
model.$watch()
},
//這是在define函數外
model.$watch("allChecked", function(completed) {//點擊上方checkbox時
model.todos.forEach(function(todo) {
todo.completed = completed
})
})
<section id="main" ms-visible="todos.size()" >
<input id="toggle-all" type="checkbox" ms-duplex-radio="allChecked">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li ms-repeat-todo="todos" ms-class="completed: todo.completed" ms-class-1="editing: $index === editingIndex">
<div class="view">
<input class="toggle" type="checkbox" ms-duplex-radio="todo.completed" data-duplex-changed="checkOne">
<label ms-dblclick="editTodo($index)">{{todo.title}}</label>
<button class="destroy" ms-click="$remove"></button>
</div>
<form>
<input class="edit" ms-duplex="todo.title" ms-blur="doneEditing" >
</form>
</li>
</ul>
</section>

接着我們做頁腳部分,這是有幾個按鈕,一個用來刪除所有選中的todo,幾個是用鏈接摸擬的,用於切換狀態,還有一段描述文本。它們涉及到一個數組,用於裝載三個狀態值,一個表示當前狀態的變量,還有兩個數值remainingCount(當前有多少個todo沒有被選中),completedCount(當前有多少個todo被選中),還有一個事件回調,用於移除removeCompleted。
難點有兩處。第一處是remainingCount與completedCount的計算,沒有什么智能的計算方法,需要我們自己寫一個方法,當數組的長度發生變化,用戶點擊了各種checkbox時觸發。
第二個是狀態值的表示,todoMVC要求它們首字母大寫,這里我引入一個自定義過濾器capitalize實現它。
<!doctype html>
<html lang="cn">
<head>
<meta charset="utf-8">
<title>avalon • TodoMVC</title>
<link rel="stylesheet" href="http://todomvc.com/architecture-examples/react/bower_components/todomvc-common/base.css">
<script src="avalon.js"></script>
<style>
.ms-controller{
visibility: hidden;
}
</style>
<script>
avalon.filters.capitalize = function(a) {
return a.charAt(0).toUpperCase() + a.slice(1)
}
var model = avalon.define({
$id: "todo",
newTodo: "",
todos: [],
addTodo: function(e) {
e.preventDefault()//阻止頁面刷新
var newTodo = model.newTodo.trim()
if (!newTodo.length) {
return
}
model.todos.push({
title: newTodo,
completed: false
});
model.newTodo = ""//清空內容
},
editingIndex: NaN,
editTodo: function($index) {
model.editingIndex = $index
//為了用戶體驗,有時不得不寫一些DOM處理
var el = this.parentNode.parentNode
setTimeout(function() {//讓光標定位於文本之后
var input = el.querySelector("input.edit")
input.focus()
input.value = input.value
})
},
doneEditing: function() {//還原
model.editingIndex = NaN
},
allChecked: false,
checkOne: function() {//點擊UI列表的checkbox時
model.$unwatch()
model.allChecked = model.todos.every(function(val) {
return val.completed
})
model.$watch()
updateCount()
},
state: "all",
status: ["all", "active", "completed"],
remainingCount: 0,
completedCount: 0,
removeCompleted: function() {
model.todos.removeAll(function(el) {
return el.completed
})
}
})
function updateCount() {
model.remainingCount = model.todos.filter(function(el) {
return el.completed === false
}).length
model.completedCount = model.todos.length - model.remainingCount;
}
model.$watch("allChecked", function(completed) {//點擊上方checkbox時
model.todos.forEach(function(todo) {
todo.completed = completed
})
updateCount()
})
model.todos.$watch("length", updateCount)
</script>
</head>
<body ms-controller="todo" class="ms-controller">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<form id="todo-form" ms-submit="addTodo" autocomplete="off">
<input id="new-todo" ms-duplex="newTodo" placeholder="What needs to be done?" autofocus>
</form>
</header>
<section id="main" ms-visible="todos.size()" >
<input id="toggle-all" type="checkbox" ms-duplex-radio="allChecked">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list">
<li ms-repeat-todo="todos" ms-class="completed: todo.completed" ms-class-1="editing: $index === editingIndex">
<div class="view">
<input class="toggle" type="checkbox" ms-duplex-radio="todo.completed" data-duplex-changed="checkOne">
<label ms-dblclick="editTodo($index)">{{todo.title}}</label>
<button class="destroy" ms-click="$remove"></button>
</div>
<form action="javascript:void(0)" ms-submit="doneEditing">
<input class="edit" ms-duplex="todo.title" ms-blur="doneEditing" >
</form>
</li>
</ul>
</section>
<footer id="footer" ms-visible="todos.size()">
<span id="todo-count">
<strong >{{remainingCount}}</strong>
item{{remainingCount>1 ? "s" : ""}} left
</span>
<ul id="filters">
<li ms-repeat="status">
<a ms-class="selected: state == el" href="#/{{el}}" >{{ el | capitalize }}</a>
</li>
</ul>
<button id="clear-completed" ms-visible="completedCount" ms-click="removeCompleted">
Clear completed ({{completedCount}})
</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://github.com/RubyLouvre/avalon">司徒正美</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</body>
</html>

這就完了,當然有人可能問如何切換狀態,在todoMVC里是使用路由系統實現,當我完善了自帶的路由系統時再補上吧。總結一下,用avalon來實現todoMVC,所有JS代碼與HTML行數是最少的。JS代碼包括avalon庫與用戶寫的代碼。像angular,backbone, polymer雖然吹得這么響,它們在實現todoMVC時有許多部分是非常不直觀的,冗長的,唯有avalon是最好的。
