迷你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是最好的。

你可能感兴趣的:(mvc)