`
kidneyball
  • 浏览: 328967 次
  • 性别: Icon_minigender_1
  • 来自: 南太平洋
社区版块
存档分类
最新评论

Knockout.js与Primefaces整合日志 4

阅读更多
解决了JSF组件添加属性的问题,现在其实已经可以比较流畅地在JSF页面中使用knockout了。

常用的交互方式有两种,一是提交时使用正常的命令组件(p:commandButton, p:commandLink等),响应时使用Primefaces的RequestContext或OmniFaces的Ajax工具类返回JSON并更新ViewModel。

举例来说,以下是一段纯粹的JSF代码
<h:form>
  <h:panelGroup id="countMessage">
    <h:outputText value="#{myBean.selected.size()"/> Row(s) selected.
  </h:panelGroup>
  <p:dataGrid ... >
    <p:ajax event="rowSelect" action="#{myBean.selectionChanged}" update="countMessage,actionPanel"/>
    <p:ajax event="rowUnselect" action="#{myBean.selectionChanged}" update="countMessage,actionPanel"/>
    <!-- Columns -->
    ...
  </p:dataGrid>
  <h:panelGroup id="actionPanel">
    <p:commandButton id="deleteBtn" value="Delete" disabled="#{myBean.selected.size() eq 0}" action="#{myBean.deleteSelected}"/>
  </h:panelGroup>
</h:form>


它的功能是当选中dataGrid中的一行或多行时,显示有多少行被选中了,并且激活Delete按钮让用户选择。上面代码的问题是显而易见的,首先是无论多简单的动作,都必须通过ajax请求服务器端进行局部渲染。其次是可以看到上面代码中只有myBean.selected集合的尺寸在改变,但由于与其相关的界面元素位于不同位置,所以在每次改变selected集合时,都要在发起请求的组件上显式通知这些区域进行重新渲染。上面的例子只涉及到两个组件,渲染两处目标区域,但实际应用很可能涉及更多的组件和更多的目标区域。在实际开发中,往往为了方便而放弃精确的指向确实需要渲染的位置,而是渲染一个较大的区域(比如在上例中,改为update整个form),这样一来却又增加了不必要的网络流量负担。

那么改为结合knockout.js实现,将会是这样的:
<h:form data-vm="UserListVM">
  <div>
    <span data-bind="text: selectedCount"> </span> Row(s) selected.
  </div>
  <p:dataGrid ... >
    <p:ajax event="rowSelect" action="#{myBean.selectionChanged}" oncomplete="app.vm.UserListVM.selectedCount(args.count)"/>
    <p:ajax event="rowUnselect" action="#{myBean.selectionChanged}" oncomplete="app.vm.UserListVM.selectedCount(args.count)"/>
    <!-- Columns -->
    ...
  </p:dataGrid>
  <h:panelGroup id="actionPanel">
    <p:commandButton id="deleteBtn" value="Delete" data-bind="attr: {disabled: selectedCount() == 0}" action="#{myBean.deleteSelected}"/>
  </h:panelGroup>
  <script>
    app.createVM({
        selectedCount: 0
    }, "UserListVM").bind("[data-vm=UserListVM]")
  </script>
</h:form>


在服务器端,我们需要调用
    RequestContext.getCurrentInstance().addCallbackParam("count", this.selected.size())

来构造响应的json数据。

咋看上去,似乎代码量还多了。但这种写法有以下好处:
1. 即使我们将来继续添加与selectedCount相关的界面元素,两个p:ajax的代码都不用改动了
2. 有需要的话,我们可以非常方便地调用客户端API实现模拟多行选中的效果 (直接调用app.vm.UserListVM.selectedCount(数字)就可以更新各处界面元素)
3. 服务器端的响应只是一个非常简短的json ({count:数字}),而不是各处目标区域的html代码。

在这个基础上,下面我们来对客户端的knockout体系进行一些扩展,让其更为易用。前面已经提过,与Avalon相比,原生的knockout.js库缺少一些提高易用性的功能,但好在knockout非常易于扩展,现在我们来把这些功能补上。



##显示/隐藏 的动画效果##

在显示/隐藏元素时使用动画效果是最常见的功能,因此我们加入slideVisible与fadeVisible两个自定义绑定:

;(function($){
    ko.bindingHandlers.slideVisible = {
            init: function(element, valueAccessor) {
                var value = ko.unwrap(valueAccessor());
                if (!value) $(element).hide()
            },
            update: function(element, valueAccessor, allBindings) {
                var value = ko.unwrap(valueAccessor());
                var duration = allBindings.get('duration') || 400;
                if (value)
                    $(element).slideDown(duration);
                else
                    $(element).slideUp(duration);
            }
        };
})(jQuery)

;(function($){
    ko.bindingHandlers.fadeVisible = {
            init: function(element, valueAccessor) {
                var value = ko.unwrap(valueAccessor());
                if (!value) $(element).hide()
            },
            update: function(element, valueAccessor, allBindings) {
                var value = ko.unwrap(valueAccessor());
                var duration = allBindings.get('duration') || 400;
                if (value)
                    $(element).fadeIn(duration);
                else
                    $(element).fadeOut(duration);
            }
        };
})(jQuery)


使用data-bind="slideVisible: condition"绑定滑动效果,data-bind="fadeVisible: condition"绑定淡出效果。并且可以配合duration绑定来指定动画时间:data-bind="fadeVisible: condition, duration: 200"



##鼠标移入##

旧式浏览器不支持:hover伪类,因此我们引入一个hover绑定,当鼠标移入绑定元素时,添加指定的class。移出时去除。


;(function($) {
    ko.bindingHandlers.hover = {
            init: function(element, valueAccessor) {
                var className = ko.unwrap(valueAccessor())
                $(element).hover(
                    function() {$(this).addClass(className)},
                    function() {$(this).removeClass(className)}
                )
            }
        };
})(jQuery)




用法
<!-- 鼠标悬浮时给DIV加上.hover class -->
<div data-bind="hover: 'hover'">...</div>




##几个常用属性的简写绑定##

knockout.js对所有元素属性统一使用attr来绑定,比如
<button data-bind="attr: {disabled: !valid()}"> </button>


比较冗长,我们可以为href, src, disabled这几个常用属性提供专门的绑定。

ko.bindingHandlers.href = {
    update: function (element, valueAccessor) {
        ko.bindingHandlers.attr.update(element, function () {
            return { href: valueAccessor()}
        });
    }
};

ko.bindingHandlers.src = {
    update: function (element, valueAccessor) {
        ko.bindingHandlers.attr.update(element, function () {
            return { src: valueAccessor()}
        });
    }
};

ko.bindingHandlers.disabled = {
    update: function (element, valueAccessor) {
        ko.bindingHandlers.attr.update(element, function () {
            return { disabled: valueAccessor()}
        });
    }
};


现在我们可以这样写
    <button data-bind="disabled: !valid()"> </button>



##反义绑定##

从上面的例子可以看出,我们往往需要使用data-bind="disabled: !valid()"这样在语义上双重否定的写法,既不方便也不直观。因此我们为常用的disabled和visible这两个布尔类型的绑定提供其相反的绑定:

ko.bindingHandlers.enabled = {
    update: function (element, valueAccessor) {
        var value = ko.unwrap(valueAccessor());
        ko.bindingHandlers.attr.update(element, function () {
            return { disabled: !value}
        });
    }
};

ko.bindingHandlers.hidden = {
    update: function (element, valueAccessor) {
        var value = ko.unwrap(valueAccessor());
        ko.bindingHandlers.visible.update(element, function () { return !value; });
    }
};


现在我们可以这样写
<button data-bind="enable: valid"> </button>




##开关绑定##

让用户选择开关的最常用做法是使用checkbox
<input type="checkbox" data-bind="checked: isSendEmail"/>


但在现实中,出于UI设计考虑,我们往往需要用div来模拟
<div data-bind="css: {checked: isSendEmail}, click: function() {isSendEmail(!isSendEmail())}">...</div>


显然这个function() {isSendEmail(!isSendEmail())}的写法略显丑陋,我们可以引入一个toggle绑定来对付这种需求。

ko.bindingHandlers.toggle = {
    init: function (element, valueAccessor) {
        var value = valueAccessor()
        ko.applyBindingsToNode(element, {
            click: function () {
                value(!value())
            }
        });
    }
};


现在可以这样写:
<div data-bind="css: {checked: isSendEmail}, toggle: isSendEmail">...</div>




##扩展css样式类绑定##

knockout.js自身提供的css样式类绑定有两种,一是根据布尔值添加或移除class
<div data-bind="css: {valid: isValid(), 'ui-disabled': isDisabled()}">


也可以直接把一个字符串值作为class添加进去,并且当值改变时移除旧的class。
<div data-bind="css: severity">


但这种做法有两个缺陷:
1. 会把元素上用class属性定义的静态样式类冲掉。
2. 如果有多个class动态改变,必须在模型中先把class的字符串拼装好,再一次性绑上去,非常不直观。

所以考虑引入一组styleClass-*绑定,其中*是1到9的数字,可以使用这套绑定直接分别绑定字符值。当然如果只需要绑定一个时也可以只使用styleClass。

;(function() {
    var factory = function(n) {
        return {
            init: function(element, valueAccessor) {
                var classNames = ko.unwrap(valueAccessor())
                var el=$(element);
                el.addClass(classNames).data("prev-class-" + n, classNames)
            },
            update: function(element, valueAccessor) {
                var el = $(element)
                var prev = el.data("prev-class-" + n)
                var current = " " + ko.unwrap(valueAccessor()) + " "
                var toRemove = ko.utils.arrayFilter((prev || "").split(/\s+/), function(item) {
                    return item.trim().length > 0 && current.indexOf(" " + item + " ") < 0
                }).join(" ")
                if (toRemove.length > 0) el.removeClass(toRemove)
                el.addClass(current)
            }
        };
    }
    
    ko.bindingHandlers.styleClass = factory(0)
    for (var i = 1; i < 10; i++) {
        ko.bindingHandlers['styleClass' + i] = factory(i)
    }
})(jQuery)


用法:
<div data-bind="styleClass: severity()">...</div>

<div data-bind="styleClass: severity(), styleClass-1: progressStatus, styleClass-2: removed() ? 'removed' : ''"


至此,针对knockout.js自身的简单扩展就差不多了。下面开始重头戏,扩展knockout.js令其更适合与JSF协作。



##从元素值反向初始化模型值##

前面已经提过,knockout.js原本的设计理念是由js驱动页面。所有模型值都需要先在javascript层面初始化,否则就会冲掉元素上的值。通常这是通过载入静态模板页面后再发一个加载数据的请求来实现的。但JSF本身就是动态页面,而且组件绑定服务器端模型值,在首次渲染就把值渲染进去了。再加载一次是画蛇添足,如果不用额外请求加载,就需要在页面上把数据用javascript形式再渲染一次。这种做法不但累赘,而且容易受到脚本注入攻击。

所以,最方便的做法是,加入一个keepInitialValue的绑定,让knockout.js在首次绑定时反过来用元素值来初始化模型值。这样我们只需要保证视图模型的架子搭起来就行了,不需要在javascript层面上用业务数据进行初始化。

显然,有这种需求的必然是一些绑定到可写监控值上的双向绑定,比如说textInput, checked, value, selectedOptions等。我们逐个来扩展

首先是textInput和value,它们的机制相似,可以放到一起处理。

//Wrap the textInput and value handler to handle keepInitialValue binding 
;(function($) {
    var textInputHandler = ko.bindingHandlers.textInput;
    var valueHandler = ko.bindingHandlers.value;
    var initViewModel = function(element, valueAccessor, allBindings) {
        if (allBindings.has("keepInitialValue")) {
            if (ko.isWritableObservable(valueAccessor())) {
                valueAccessor()($(element).val())
            }
        }
    }
    
    ko.bindingHandlers.textInput = { 
        'init': function (element, valueAccessor, allBindings) {
            initViewModel(element, valueAccessor, allBindings)
            textInputHandler.init(element, valueAccessor, allBindings)
        }
    }
    ko.bindingHandlers.textInput.__proto__ = textInputHandler
    
    ko.bindingHandlers.value = {
        'init': function (element, valueAccessor, allBindings) {
            initViewModel(element, valueAccessor, allBindings)
            valueHandler.init(element, valueAccessor, allBindings)
        }
    }
    ko.bindingHandlers.value.__proto__ = valueHandler
})(jQuery);


绑定用法如下:
    <h:inputText value="#{myBean.firstName}" data-bind="textInput: firstName, keepInitialValue"/>


在javascript中,我们只需要定义好firstName属性,随便给它一个初始值(最好保证类型相同)就行了。
app.registerConstructor("MyBeanVM", function() {
    this.firstName = ko.observable("")
})


同样,我们下面处理checked属性,由于checked属性要兼顾radio与checkbox两种元素,并且checkbox又分为绑定布尔和绑定数组两种情况,相对复杂一点

//Wrap the checked handler to handle keepInitialValue binding 
;(function() {
    var checkedHandler = ko.bindingHandlers.checked;
    var initViewModel = function(element, valueAccessor, allBindings) {
        function updateModel() {
            /* A simplified version of internal helper function in ko.bindingHandlers.checked */

            var isCheckbox = element.type == "checkbox",
                isRadio = element.type == "radio",
                isChecked = element.checked;
            var useCheckedValue = isRadio || isValueArray;
            var elemValue = useCheckedValue ? checkedValue() : isChecked;

            // We can ignore unchecked radio buttons, because some other radio
            // button will be getting checked, and that one can take care of updating state.
            if (isRadio && !isChecked) {
                return;
            }
            var isValueArray = isCheckbox && (ko.utils.unwrapObservable(valueAccessor()) instanceof Array)
            if (isValueArray) {
                app.console.error('"keepInitialValue" does not support array value on the "checked" binding.')
            } else {
                valueAccessor()(elemValue)
            }
        };

        if (allBindings.has("keepInitialValue")) {
            if (ko.isWritableObservable(valueAccessor())) {
                updateModel()
            }
        }
    }
    
    ko.bindingHandlers.checked = { 
        'init': function (element, valueAccessor, allBindings) {
            initViewModel(element, valueAccessor, allBindings)
            checkedHandler.init(element, valueAccessor, allBindings)
        }
    }
    ko.bindingHandlers.checked.__proto__ = checkedHandler
})();



最后,处理selectedOptions

;(function($) {
    var selectedOptionsHandler = ko.bindingHandlers.selectedOptions;
    ko.bindingHandlers.selectedOptions = { 
        'init': function (element, valueAccessor, allBindings) {
            if (allBindings.has("keepInitialValue")) {
                (function () {
                    var value = valueAccessor(), valueToWrite = [];
                    ko.utils.arrayForEach(element.getElementsByTagName("option"), function(node) {
                        if (node.selected)
                            valueToWrite.push(ko.selectExtensions.readValue(node));
                    });
                    value(valueToWrite);
                })();
            }
            selectedOptionsHandler.init(element, valueAccessor, allBindings)
        },
        'update': function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            selectedOptionsHandler.update(element, valueAccessor, allBindings, viewModel, bindingContext)
        }
        
    }
    ko.bindingHandlers.selectedOptions.__proto__ = selectedOptionsHandler
})(jQuery);


大功告成,现在主要的几种双向绑定都支持keepInitialValue绑定了。



##绑定内部元素##

虽然我们通过替换ResponseWriter的方式解决了大部分JSF组件渲染data-bind属性的问题。但还是对以下两种场景无能为力:
1. 某些渲染出复杂内部结构的组件,我们想绑定其内部某个元素时。比如说,绑定日期组件的年份下拉框。
2. 某些由父组件负责其子组件渲染的组件,我们想绑定子组件时。比如说,想绑定p:menuButton的其中一个p:menuItem,你会发现,p:menuItem对应的li元素根本没有clientId,它是由p:menuButton组件负责渲染的。

这类情况,最原始的解决办法是在实施绑定之前,用jQuery把data-bind强行加到元素上。但这样一来,元素标签和data-bind就分开了,代码变得很不直观。一个更好的解决方案是,引入一组data-bind-child-* (其中*为数字)绑定,把绑定子元素属性的绑定放到邻近的父元素上,这样至少保证了绑定的定义和绑定的目标不会离开太远。自然,为了触发knockout框架对这些属性进行处理,我们需要引入一个标志性的knockout绑定,命名为bindChildren。也就是说,当元素的data-bind中包含bindChildren绑定时,就触发对元素上data-bind-child-*属性的处理。在每个data-bind-child-*属性中,使用“绑定内容@子元素选择器”的格式,把绑定内容添加到符合选择器的子元素上。

比如说如果要绑定一个菜单项,可以这样写:
<div  data-bind="bindChildren" data-bind-child-1="visible: selectedCount > 0 @ li.delete-all">
    <p:menuButton>
        <p:menuItem styleClass="delete-all" value="Delete All" action="#{myBean.deleteAll}"/>
    </p:menuButton>
</div>


bindChildren绑定的实现代码如下:
;(function($){
    ko.bindingHandlers.bindChildren = {
            init: function(element) {
                $.each(element.attributes, function() {
                    try {
                        var attr = this, name, value, parts
                        if (attr.specified) {
                            name = attr.name
                            if (name.match(/^data-bind-child(?:-\d+)?$/i)) {
                                value = attr.value
                                parts = value.split(/\s*@\s*/)
                                if (parts.length == 2) {
                                    $(element).find(parts[1]).each(function() {
                                        $(this).attr("data-bind", parts[0])
                                    })
                                } else {
                                    app.console.error("Invalid data-bind-child content: " + value)
                                }
                            }
                        }
                    } catch (err) {
                        app.console.error(err)
                    }
                })
            }
    }
})(jQuery)




##跳过区域##

knockout.js不支持视图模型嵌套,如果试图在同一个元素试图实施绑定两次,就会抛出错误。在JSF中,由于大量使用Facelet来组合页面,往往很难避免区域的嵌套。比如说在页面A的绑定区域中嵌入了页面B,而页面B又包含了一个绑定区域。那么当页面A的绑定区域完全渲染完毕,开始进行绑定时,其内部属于页面B的那个区域事实上已经绑定过了,这样A的绑定就会中途出错无法完成。

为了避免这种情况,我们需要引入一个告诉knockout.js跳过某个区域的标志,这样我们就可以用这个标志把区域B保护起来,其内部由页面B的代码独立绑定。而在对A进行绑定时,则跳过此区域。

好在knockout.js其实已经提供了非常方便的解决方案,但却没有提供现成的绑定,需要我们自己定义。

ko.bindingHandlers.stop = ko.bindingHandlers.noKnockout = {
        init: function() {
            return { controlsDescendantBindings: true };
        }
}

ko.virtualElements.allowedBindings.stop = true

ko.virtualElements.allowedBindings.noKnockout = true


在这里我们定义了stop和noKnockout两个绑定,实现代码和功能是完全一样的:告诉knockout.js跳过其内部区域。但在业务语义上有所不同:
* stop用来保护嵌套的内部区域不被外部区域误绑定。也就是解决上面提到的问题
* noKnockout用来提醒开发者,其内部不应该有任何knockout绑定。由于前面已经提到过,被knockout绑定组件不能单独进行局部渲染(局部渲染后必须重新绑定),我们可以使用noKnockout来提醒开发者,这个区域内部不会有任何knockout绑定的元素,可以自由使用JSF的局部渲染。当然这只是一种构想的用法,实际效果如何需要在实际使用中评估。

用法示例如下:

页面A
<div data-vm="vmA">
...
    <ui:include src="b.xhtml"/>
...
</div>
<script>
app.createVM("vmA").bind("[data-vm=vmA]")
</script>


页面B
<div data-bind="stop">
    <div data-vm="vmB">
    ...
    </div>
    <script>
        app.createVM("vmB").bind("[data-vm=vmB]")
    </script>
</div>


至此,在客户端针对JSF整合的扩展就差不多了。下一节我们将回过头去完善服务器端的API。
分享到:
评论

相关推荐

    MVC使用Knockout.JS实现的单页应用程序(SPA)1

    4. **实现视图**:创建HTML视图,使用Knockout.JS的数据绑定特性来连接视图与ViewModel。在视图中,使用Knockout的`data-bind`属性将HTML元素与ViewModel的属性绑定。例如,可以显示FIFA World Cup赛事的标题、日期...

    深入学习 Knockout.JS

    Knockout.js是一个非常流行的JavaScript库,它遵循MVVM(模型-视图-视图模型)设计模式,主要被用来帮助开发者实现JavaScript应用程序中丰富的用户界面。在本文中,我们将详细介绍Knockout.js的核心概念以及如何在...

    基于ASP.NET MVC 4 +Knockout.JS+Web API+FluentData+EasyUI 通用权限管理

    Knockout.JS 是一个JavaScript库,专门用于实现MVVM(Model-View-ViewModel)模式,简化了前端数据绑定和动态用户界面的创建。在ASP.NET MVC 4项目中,Knockout.JS可以无缝集成,帮助开发者实现在客户端实时更新视图...

    MVC使用Knockout.JS实现的单页应用程序(SPA)2

    Knockout.JS是一款强大的JavaScript库,它提供了数据绑定和依赖跟踪功能,使得在浏览器端创建复杂的用户界面变得更加简单。而ASP.NET MVC是一个流行的服务器端框架,用于构建动态、数据驱动的Web应用。 首先,了解...

    前端项目-knockout.mapping.zip

    这个插件是为了解决在使用Knockout.js进行数据绑定时,对JavaScript对象与视图模型之间映射的问题。Knockout.js是一个MVVM(Model-View-ViewModel)框架,它使得JavaScript可以轻松地实现数据双向绑定,简化了DOM...

    mvc+knockout.js联动

    3. **服务器端与客户端通信**:在MVC4中,控制器可以返回JSON或ViewModel对象,供Knockout.js在客户端使用。通常,这可以通过AJAX请求实现,例如使用jQuery的`$.ajax`或`$.getJSON`。 4. **页面初始化**:在HTML...

    ASP.NET MVC 5 with Bootstrap and Knockout.js Oreilly 2015

    4. Knockout.js实战:理解MVVM模式,掌握数据绑定、依赖跟踪和订阅事件等核心概念。 5. 模板和部分视图:如何复用HTML片段,提高代码复用性。 6. AJAX和jQuery:使用jQuery进行异步请求,更新页面部分内容。 7. 安全...

    Knockout.js Succinctly

    ### Knockout.js Succinctly:全面解析与应用实践 #### 一、Knockout.js简介与特点 **Knockout.js**是一款轻量级的客户端MVC(Model-View-Controller)框架,专为简化现代Web应用程序开发而设计。其核心功能之一是...

    Getting Started with Knockout.js for .NET Developers(PACKT,2015)

    Beginning with a vital overview of Knockout.js, including the MVVM design pattern, you will create a simple but powerful application capable of integrating with ASP.NET MVC as well as gain a thorough ...

    基于 BOOTSTRAP 和 KNOCKOUT.JS 的 ASP.NET MVC 开发实战

    基于 BOOTSTRAP 和 KNOCKOUT.JS 的 ASP.NET MVC 开发实战。 利用动态服务端Web内容和响应Web设计共同构建的网站,在任何分辨率、桌面或移动设备下都可以进行良好的显示。通过本书的实践应用,你将可以学习对ASP.NET ...

    ASP.NET.MVC.5.with.Bootstrap.and.Knockout.js.1491914394

    With this practical book, you’ll learn how by combining the ASP.NET MVC server-side language, the Bootstrap front-end framework, and Knockout.js—the JavaScript implementation of the Model-View-...

    一个非常简单的websockets/knockout.js/node.js/socket.io实时游戏_HTML_代码_下载

    简单的knockout.js/websockets/node.js实时游戏 这是一个非常简单的实时游戏,使用 node.js、knockout.js 和 socket.io 创建。 ##目标 尝试比其他玩家更快地解决简单的数学方程。 每场比赛都有时间限制(默认...

    Web App Testing Using Knockout.JS(PACKT,2014)

    Knockout.JS is an emerging JavaScript presentation framework that promotes a solid solution design in combination with Jasmine, a well-known unit testing library. They both ensure rapid development ...

    ASP.NET MVC 5 with Bootstrap and Knockout

    JavaScript捆绑与最小化 小结 第14章构建数据模型 Code—First模型 定义DbContext并初始化数据 视图模型 小结 第15章布局实现 共享布局 购物车摘要 分类菜单 小结 第16章图书列表 主页 特色图书 按...

    KnockOut 2.88 支持32及64位版.rar

    KnockOut抠图插件,支持32、64位版系统(与photoshop位数无关) 目前测试使用win10(64位)、photoshop cs6 13.0(32位) 1.下载后放在photoshop安装目录中的Plug-ins中(解压后Plug-ins文件夹中应该会包含:KnockOut.8bf...

    knockout.js---jquery.mobile.js-using-shim:Knockout.js + jquery.mobile.js

    **Knockout.js与jQuery Mobile的整合:使用Shim** 在Web开发中,Knockout.js和jQuery Mobile是两个非常流行的库,分别用于数据绑定和移动应用的UI交互。它们结合使用可以创建出功能强大且用户友好的动态移动界面。...

    Knockout js 基礎入門教學

    Knockout.js能够很好地与其他JavaScript库和框架协同工作,例如可以轻松地将Bootstrap或其他UI框架的组件与Knockout.js的绑定相结合。 #### 更改button的状态 通过绑定`disabled`属性,可以基于数据条件动态地启用...

Global site tag (gtag.js) - Google Analytics