迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

时间:2023-03-09 17:07:51
迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

大凡出名的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下浏览。

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

我们添加一些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>

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

好了,有模有样了。我们添加一些实际功能了。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>

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

我们看到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>

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

接着来做全选非选功能,我们需要一个变量来保存全选状态,然后监听它来同步下面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>

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

接着我们做页脚部分,这是有几个按钮,一个用来删除所有选中的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>

迷你MVVM框架 avalonjs 学习教程18、一步步做一个todoMVC

这就完了,当然有人可能问如何切换状态,在todoMVC里是使用路由系统实现,当我完善了自带的路由系统时再补上吧。总结一下,用avalon来实现todoMVC,所有JS代码与HTML行数是最少的。JS代码包括avalon库与用户写的代码。像angular,backbone, polymer虽然吹得这么响,它们在实现todoMVC时有许多部分是非常不直观的,冗长的,唯有avalon是最好的。