Knockout学习之创建自定义绑定

创建自定义绑定
在Knockout对MVVM的解释中,绑定是连接View和ViewModel的中介。
他们(绑定)可以执行双向更新:
①绑定会监听ViewModel(可以理解为数据)的变化,并对应的更新View的DOM。
②绑定会捕获DOM的事件并相应的更新ViewModel的属性(数据)。

Knockout 有一套灵活且全面的内置绑定属性(如text,click,foreach)。
但是它还不仅仅如此--你只通过几行代码就可以创建自定义绑定(属性)

OK,现在我们可以试试造两个自定义的绑定了。

首先你将看到了一个没什么亮点但很功能齐全的调查页面。
这是通过前两篇的知识完成的一个简单Demo。

<!DOCTYPE HTML>
<html>
<head>
    <title>Custom Bindings</title>
    <script src="../JS/jquery-latest.min.js" type="text/javascript"></script>
    <script src="knockout-2.2.0.js" type="text/javascript"></script>
    <style type="text/css">
        body { font-family: Helvetica, Arial }
        input:not([type]), input[type=text], input[type=password], select { background-color: #FFFFCC; border: 1px solid gray; padding: 2px; }
        
        table { background-color: #cde; padding: 1em; border-radius: 0.5em; }
        table th { text-align:left; }
        table th:last-child { min-width: 130px; }
    </style>
</head>
<body>
    <h3 data-bind="text:question"></h3>
    <p>请分配 <b data-bind="text:pointsBudget"></b>点到一下选项.</p>
    <table>
        <thead>
            <tr><th>选项</th><th>重要度</th></tr>
        </thead>
        <tbody data-bind="foreach:answers">
            <tr>
                <td data-bind="text:answerText"></td>
                <td>
                    <select data-bind="options:[1,2,3,4,5],value:points"></select>
                </td>
            </tr>
        </tbody>
    </table>
    
    <h3 data-bind="visible:pointsUsed() > pointsBudget">点数已用光!删除点吧。</h3>
    <p>您剩余的可用点数为:<b data-bind="text:pointsBudget - pointsUsed()"></b></p>
    <button data-bind="enable:pointsUsed() <= pointsBudget,click:save">提交</button>

    <script type="text/javascript">
        
        //Model
        function Answer(text) {
            this.answerText = text;
            this.points = ko.observable(1);
        }

        //ViewModel
        function SurveyViewModel(question, pointsBudget, answers) {
            this.question = question;
            this.pointsBudget = pointsBudget;
            this.answers = $.map(answers, function(text) {
                return new Answer(text);
            });
            this.save = function() {
                alert("To Do");
            };
            this.pointsUsed = ko.computed(function() {
                var total = 0;
                for (var i = 0; i < this.answers.length; i++) {
                    total += this.answers[i].points();
                }
                return total;
            }, this);
        }

        //Bind
        ko.applyBindings(new SurveyViewModel(
            "哪些因素会影响你的技术选择?"
            , 10
            , [
                "Functionality,compatibility,pricing-all that boring stuff"
                , "How often it is mentioned on Hacker News"
                , "Number of gradients/dropshadows on project homepage"
                ,"Totally believable testimonials on project homepage"
            ]
        ));
    </script>
</body>
</html>

现在我们通过三种方式来提高这个页面的体验。

  • 给警告信息-"You've used too many points"添加动画过渡
  • 改进[Finished]按钮的样式
  • 用比较有趣的[星星等级]代替无聊的下拉菜

 ①添加动画过渡

当访客分配点数超支时,警告-“点数已用光!删除点吧。”很不平滑的显示出来,因为它的显示依赖于[visible]属性绑定。
如果你想让警告文字平滑的淡入淡出,可以写一个快捷的,可重用的自定义绑定属性,使用jQuery的fade方法实现动画效果。

先通过给ko.bindingHandlers对象指定一个新属性来定义一个自定义绑定属性。该属性会暴露两个回调函数:
init:当该绑定第一次发生时调用(设置初始状态或注册事件处理器是有必要的)
update:只要相关联的数据发生变化就会被调用(然后就可以更新相应的DOM了)

通过在ViewModel顶部添加以下代码定义一个[fadeVisible](平滑可见)绑定

ko.bindingHandlers.fadeVisible = {
    update:function(ele,valueAccessor){
        var shouldDisplay = valueAccessor();
        shouldDisplay?$(ele).fadeIn():$(ele).fadeOut();
    }
};

如你所见,[update]处理器被赋予了两个参数--被绑定的元素,返回关联数据当前值的函数。
基于那个当前值,我们可以用jQuery让元素淡入或淡出。

用我们刚完成的自定义绑定就可以简单的修改那个警告文字,用fadeVisible代替visible。

<h3 data-bind="fadeVisible:pointsUsed() > pointsBudget">点数已用光!删除点吧。</h3>

现在如果运行的话淡入淡出的效果就应该完美实现了。真的么?好像初始化的时候它会淡入一次,然后才淡出。

设置元素初始状态
出现刚才的超出预期的现象,原因就是我们没有给出绑定的初始状态。
所以,我们需要用一个[init]处理器确保元素初期状态匹配ViewModel数据的初期值

ko.bindingHandlers.fadeVisible = {
    init: function(ele, valueAccessor) {
        //Start visible/invisible according to initial value
        var shouldDisplay = valueAccessor();
        $(ele).toggle(shouldDisplay);
    },

    update: function(ele, valueAccessor) {
        //更新的时候,淡入/淡出
        var shouldDisplay = valueAccessor();
        shouldDisplay ? $(ele).fadeIn() : $(ele).fadeOut();
    }
};

小功告成!虽说这个自定义的绑定貌似太小了点,但它是完全可复用的,可以把所有自定义的绑定放到一个单独的文件中,这样以后就方便使用了。


②集成第三方组件

刚才我们自定义的绑定是关于动画效果的属性。如果还想让View中包含一些第三方UI组件(jQueryUI/YUI/LigerUI...),并且绑定到ViewModel属性上,
最简单的方法就是创建一个自定义绑定,介于你的ViewModel和第三方组件之间。

我们用jQuery UI的"Button"部件来提高"Finish"按钮的用户体验。

首先定义一个绑定jqButton--将以下代码添加到ViewModel顶部。

ko.bindingHandlers.jqButton = {
    init: function(ele) {
        $(ele).button();
    },
    update: function(ele, valueAccessor) {
        var currentValue = valueAccessor();
        //jQuery UI中设置属性的一种方式。
        $(ele).button("option","disabled",currentValue.enable === false);
    }
};

修改页面上的"Finish"按钮

<button data-bind="jqButton:true,enable:pointsUsed() <= pointsBudget,click:save">提交</button>

这样,基于第三方UI的可复用的自定义绑定就完成了。(别忘了添加对jQuery UI js和CSS的引用)


③实现自定义部件(点亮星星)

现在我们来让页面变得更有趣一点。(整天对着下拉菜单是件很蛋疼的事)
用星级系统来代替select。
其实这种东西在网上一抓一大把(example),但为了学习,我们从头来过。

首先要定义starRating绑定,为此要添加以下代码到ViewModel顶部:

ko.bindingHandlers.starRating = {
    //初期化时,改变DOM内容及属性从而渲染元素
    init: function(ele, valueAccessor) {
        $(ele).addClass("starRating");
        for (var i = 0; i < 5; i++) {
            $("<span>").appendTo(ele);
        }
    },
    //根据当前数据指定适当的CSS
    update: function(ele, valueAccessor) {
        var observable = valueAccessor();
        $("span", ele).each(function(index) {
            //$.toggleClass(className,[switch])
            $(this).toggleClass("chosen",index < observable());
        });
    }
};

这段代码插入了一堆span元素。为了将他们渲染成星星,我们也必须准备一段CSS(在完整的代码中会看到)。
再修改一下绑定的属性,这样我们就可以代替select了。

<tbody data-bind="foreach:answers">
    <tr>
        <td data-bind="text:answerText"></td>
        <td data-bind="starRating:points"></td>
    </tr>
</tbody>

当mouse hover时,让图片高亮
当用户将光标移动到星星上时,将他们要选中的星星高亮是个不错的idea。
而此种高亮状态没必要保存到ViewModel(我们只保存用户选择了的数据),
最简单的方式就是通过jQuery来完成这个效果。

....some code
init: function(ele, valueAccessor) {
    var observable = valueAccessor();
    $("span", ele).each(function(index) {
        $(this).hover(
            function() {
                $(this).prevAll().add(this).addClass("hoverChosen");
            },
            function() {
                $(this).prevAll().add(this).removeClass("hoverChosen");
            }
        );
    });
}

现在鼠标移到哪高亮就到哪!

保存数据到ViewModel
当用户点击某个星星时,我们应该在ViewModel中保存这一选择状态,以便能动态更新UI。
这也不难:用jQuery的点击方法来捕获用户的点击星星这一事件。

....some code
init: function(ele, valueAccessor) {
    var observable = valueAccessor();
    $("span", ele).each(function(index) {
        $(this).hover(
            function() {
                $(this).prevAll().add(this).addClass("hoverChosen");
            },
            function() {
                $(this).prevAll().add(this).removeClass("hoverChosen");
            }
        ).click(function() {
            var observable = valueAccessor();
            observable(index + 1);
        });
    });
}

收工!贴上完整代码

<!DOCTYPE HTML>
<html>
<head>
    <title>Custom Bindings</title>
    <script src="../JS/jquery-latest.min.js" type="text/javascript"></script>
    <script src="knockout-2.2.0.js" type="text/javascript"></script>
    <script src="jquery-ui.min.js" type="text/javascript"></script>
    <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.14/themes/start/jquery-ui.css" />
    <style type="text/css">
        body { font-family: Helvetica, Arial }
        input:not([type]), input[type=text], input[type=password], select { background-color: #FFFFCC; border: 1px solid gray; padding: 2px; }
        
        table { background-color: #cde; padding: 1em; border-radius: 0.5em; }
        table th { text-align:left; }
        table th:last-child { min-width: 130px; }

        .starRating span { width:24px; height:24px; background-image: url(IMG/stars.png); display:inline-block; cursor: pointer; background-position: -24px 0; }
        .starRating span.chosen { background-position: 0 0; }
        .starRating:hover span { background-position: -24px 0; }
        .starRating:hover span.hoverChosen { background-position: 0 0;}
    </style>
</head>
<body>
    <h3 data-bind="text:question"></h3>
    <p>请分配 <b data-bind="text:pointsBudget"></b>点到一下选项.</p>
    <table>
        <thead>
            <tr><th>选项</th><th>重要度</th></tr>
        </thead>
        <tbody data-bind="foreach:answers">
            <tr>
                <td data-bind="text:answerText"></td>
                <td data-bind="starRating:points"></td>
            </tr>
        </tbody>
    </table>
    
    <h3 data-bind="fadeVisible:pointsUsed() > pointsBudget">点数已用光!删除点吧。</h3>
    <p>您剩余的可用点数为:<b data-bind="text:pointsBudget - pointsUsed()"></b></p>
    <button data-bind="jqButton:{enable:pointsUsed() <= pointsBudget },click:save">提交</button>

    <script type="text/javascript">
        
        //Model
        function Answer(text) {
            this.answerText = text;
            this.points = ko.observable(1);
        }

        //ViewModel
        function SurveyViewModel(question, pointsBudget, answers) {
            //第三方组件扩展
            ko.bindingHandlers.jqButton = {
                init: function(ele) {
                    $(ele).button();
                },
                update: function(ele, valueAccessor) {
                    var currentValue = valueAccessor();
                    
                    //jQuery UI中设置属性的一种方式。
                    $(ele).button("option","disabled",currentValue.enable === false);
                }
            };
            
            //自定义绑定
            ko.bindingHandlers.fadeVisible = {
                init: function(ele, valueAccessor) {
                    //Start visible/invisible according to initial value
                    var shouldDisplay = valueAccessor();
                    $(ele).toggle(shouldDisplay);
                },

                update: function(ele, valueAccessor) {
                    //更新的时候,淡入/淡出
                    var shouldDisplay = valueAccessor();
                    shouldDisplay ? $(ele).fadeIn() : $(ele).fadeOut();
                }
            };

            ko.bindingHandlers.starRating = {
                //初期化时,改变DOM内容及属性从而渲染元素
                init: function(ele, valueAccessor) {
                    var observable = valueAccessor();
                    
                    $(ele).addClass("starRating");
                    for (var i = 0; i < 5; i++) {
                        $("<span>").appendTo(ele);
                    }

                    $("span", ele).each(function(index) {
                        $(this).hover(
                            function() {
                                $(this).prevAll().add(this).addClass("hoverChosen");
                            },
                            function() {
                                $(this).prevAll().add(this).removeClass("hoverChosen");
                            }
                        ).click(function() {
                            observable(index + 1);
                        });
                    });
                },
                //根据当前数据指定适当的CSS
                update: function(ele, valueAccessor) {
                    // Give the first x stars the "chosen" class, where x <= rating
                    var observable = valueAccessor();
                    $("span", ele).each(function(index) {
                        $(this).toggleClass("chosen", index < observable());
                    });
                }
            };
            
            this.question = question;
            this.pointsBudget = pointsBudget;
            this.answers = $.map(answers, function(text) {
                return new Answer(text);
            });
            this.save = function() {
                alert("To Do");
            };
            this.pointsUsed = ko.computed(function() {
                var total = 0;
                for (var i = 0; i < this.answers.length; i++) {
                    total += this.answers[i].points();
                }
                return total;
            }, this);
        }

        //Bind
        ko.applyBindings(new SurveyViewModel(
            "哪些因素会影响你的技术选择?"
            , 10
            , [
                "功能、兼容性之类的东西"
                , "在黑客新闻上被提及的频率"
                , "项目主页上的梯度数量"
                ,"在项目主页上完全可信的客户评价"
            ]
        ));
    </script>
</body>
</html>

效果图

素材(星星图)


 

 

 

 

posted @ 2012-12-21 12:37  TiestoRay  阅读(661)  评论(0编辑  收藏  举报