创建自定义绑定
在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>
效果图
素材(星星图)