|  | 
| 
今天有得空加了点工。 itemtemplate现在支持引用,所以可以实现递归模版,也就是说支持和树状结构的数据进行绑定了。
 除了引用外,itemtemplate还支持回掉函数,也就是彻底由用户自行创建,这个在需求很变态(比如要求一个列表中的项目根据数据类型不同采用不同模版之类的)的时候特别管用。
 
 增加了新的扩展“action”,基本上就是从knot.js提供UI事件支持,和普通UI事件的区别是knotjs在事件发生时会往事件handler里传递当前的data context。本来不打算加这个支持害怕不必要的增加系统复杂度,后来想了想,有这个东西的确还是能方便很多。
 
 knot.js已经接近完成了,接下来还打算增加异步validator和独立的cbs文件的支持,之后,我认为就可以做第一次发布了。  新增这些东西后Knot.js压缩后尺寸依然极小,不过14k.
 
 做了个新的包含树状结构,master-detail结构的demo在这里, 按ctl+alt+k可以呼出调试窗口,实时监控绑定状态。:
 http://knotjs.atwebpages.com/example/treeExample.html
 
 这个demo里面的treeview完全是由Knot.js绑定到一组div上创建而成的,包括tree的展开收缩状态,全部可以由绑定来实现。关键是这个实现同时还非常简单直观。
 
 html:
 cbs复制代码<body onload="onLoad();">
<h2>Knot.js Example</h2>
<div>
    <div id="treeArea">
        <div>Group</div>
        <input id="textCategoryTitle" class="disableWhenNoCategorySelected"/>
        <div id="treeView">
            <div id="treeItem">
                <div><button>+</button><span></span></div>
                <div></div>
            </div>
        </div>
        <div>
            <button onclick="addCategory()" class="disableWhenNoCategorySelected">+</button>
            <button onclick="removeCategory()" class="disableWhenNoCategorySelected">-</button></div>
    </div>
    <div id="peopleListArea">
        <div>People</div>
        <div id="peopleList">
            <div>
                <span id="spanTitle"> </span><span id="spanName"></span>
                <hr/>
                Age:<span id="spanAge"></span>,
                From:<span id="spanFrom"></span>
            </div>
        </div>
    </div>
    <div id="editArea">
        <div>
            <div class="fieldTitle">Name:</div>
            <input class="disableWhenNoSelection" type="text"/>
            <span class="errorMessage"></span>
        </div>
        <div>
            <div class="fieldTitle">Age:</div>
            <input class="disableWhenNoSelection" type="text"/>
            <span class="errorMessage"></span>
        </div>
        <div>
            <div class="fieldTitle">Sex:</div>
            <select class="disableWhenNoSelection">
                <option>Male</option>
                <option>Female</option>
            </select>
        </div>
        <div>
            <div class="fieldTitle">From:</div>
            <select class="disableWhenNoSelection">
                <option id="selectFromOptionTemplate"></option>
            </select>
        </div>
        <p>
            <button class="disableWhenNoCategorySelected" onclick="onNewPeople();">New</button>
            <button class="disableWhenNoSelection" onclick="onRemovePeople()">Delete</button>
        </p>
    </div>
</div>
</body>
javascript 代码。绝大多数都是极为简单的逻辑,复杂部分基本上都由绑定解决掉了。复制代码<script type="text/cbs">
    body{
        knotDataContext:/model
    }
    #treeView{
        foreach:*categories =>treeItem;
    }
    #treeItem>div:first-child{
        class:*isSelected=>boolToClassConverter;
        @click: onTreeItemClicked;
    }
    #treeItem>div:first-child span{
        text:*title
    }
    #treeItem>div:last-child{
        foreach: *children =>treeItem;
        style-display: *isExpanded =>visibleControl
    }
    #treeItem button {
        text: *isExpanded =>boolToExpandSymbol;
        style-opacity: *children.length =>boolToOpacity;
        disabled: *children.length =>enableControl;
        @click: onExpandButtonClicked
    }
    .disableWhenNoCategorySelected{
        disabled: */model.selectedCategory=>enableControl;
    }
    #textCategoryTitle{
        value: */model.selectedCategory.title =!mustNotNull;
    }
    #peopleList{
        foreach:*selectedCategory.peopleList;
    }
    #peopleList>div{
        class:*isSelected=>boolToClassConverter;
        @click:onPeopleClicked
    }
    #peopleList>div span [0]{
        text:*sex=>sexToTitle
    }
    #spanName{
        text:*name
    }
    #spanAge{
        text:*age
    }
    #spanFrom{
        text:*from
    }
    #editArea input[type="text"] [0]{
        value:*selectedPeople.name =!mustNotNull & duplicatedName;
    }
    #editArea .errorMessage [0]{
        style:{
            display:!selectedPeople.name=>visibleControl
        };
        text:!selectedPeople.name
    }
    #editArea input[type="text"] [1]{
        value:*selectedPeople.age =!mustNotNull & checkInteger&ageRange;
    }
    #editArea .errorMessage [1]{
        style:
        {
            display:!selectedPeople.age=>visibleControl
        };
        text:!selectedPeople.age
    }
    #editArea select [0]{
        selected:*selectedPeople.sex;
    }
    #editArea select [1]{
        selected:*selectedPeople.from;
        foreach:/countries
    }
    .disableWhenNoSelection{
        disabled:*/model.selectedPeople=>enableControl;
    }
    #selectFromOptionTemplate{
        text:--self
    }
</script>
复制代码var model = {
        categories:[
            {title:"test", children:[], peopleList:[], isExpanded:true, parent:model}
        ],
        selectedCategory:null,
        selectedPeople:null};
    //here is everything start
    function onLoad() {
        //tie a knot! done!!
        Knot.tie();
        //when validating fails, set focus to the failed element.
        Knot.registerOnValidatingError(function (msg, node) {
            setTimeout(function () {
                if (node.focus)
                    node.focus();
            }, 1);
        });
    }
    function deselectTreeData(data, children){
        for (var i = 0; i < children.length; i++) {
            if (children[i] != data) {
                Knot.setValue(children[i], "isSelected", false);
            }
            if(children[i].children){
                deselectTreeData(data, children[i].children);
            }
        }
    }
    function onExpandButtonClicked(){
        Knot.setValue(this, "isExpanded", !this.isExpanded)
    }
    function onTreeItemClicked(){
        Knot.setValue(this, "isSelected", true);
        Knot.setValue(model, "selectedCategory", this);
        if(this.peopleList.length>0)
            selectPeople(this.peopleList[0]);
        else
            Knot.setValue(model, "selectedPeople");
        deselectTreeData(this, model.categories);
    }
    function addCategory(){
        if(!validate())
            return;
        var cate = model.selectedCategory;
        if(!cate)
            cate = model;
        var newCate = {title: "new", isExpanded:true, peopleList:[], parent:model.selectedCategory};
        if(!cate.children)
            Knot.setValue(cate, "children", []);
        Knot.addToArray(cate.children, newCate);
    }
    function removeCategory(){
        if(!model.selectedCategory)
            return;
        Knot.removeFromArray(model.selectedCategory.parent.children, model.selectedCategory);
        Knot.setValue(model.selectedCategory, null);
        Knot.setValue(model.selectedPeople, null);
    }
    //call this function to validate all data. it is called when model.selected is changed.
    function validate() {
        var errMessage = Knot.validate(document.body);
        if (errMessage) {
            alert(errMessage);
            return false;
        }
        return true;
    }
    //event handler of "new" button
    function onNewPeople() {
        if (!validate())
            return;
        //set default sex to "Male"
        var n = {sex:"Male"};
        //call Knot.addToArray, so that UI will refresh automatically
        Knot.addToArray(model.selectedCategory.peopleList, n);
        selectPeople(n);
    }
    //event handler of "remove" button
    function onRemovePeople() {
        if (model.selectedPeople) {
            var index = model.selectedCategory.peopleList.indexOf(model.selectedPeople);
            Knot.removeFromArray(model.selectedCategory.peopleList, model.selectedPeople);
            if (model.selectedCategory.peopleList.length > 0) {
                selectPeople(model.selectedCategory.peopleList[Math.min(model.selectedCategory.peopleList.length - 1, index)], true);
            }
            else {
                //Use Knot.setValue to update data, so the UI will refresh
                Knot.setValue(model, "selectedPeople", null);
            }
        }
    }
    //set the data to current selected data.
    //setting "model.selected" to update the edit controls, setting "isSelected" to update css on peopleList items
    function selectPeople(data, ignoreValidating) {
        if (!ignoreValidating && !validate())
            return;
        Knot.setValue(data, "isSelected", true);
        Knot.setValue(model, "selectedPeople", data);
        for (var i = 0; i < model.selectedCategory.peopleList.length; i++) {
            if (model.selectedCategory.peopleList[i] != data) {
                Knot.setValue(model.selectedCategory.peopleList[i], "isSelected", false);
            }
        }
    }
    function onPeopleClicked() {
        selectPeople(this);
    }
    //these converters are used to change the values on data to anything you want them on html. You can create your own, it is super easy!
    //they are associate to Knot by "=>" mark in html
    var boolToClassConverter = Knot.Converters.createBoolToValue("selected", "unselected");
    var enableControl = Knot.Converters.createBoolToValue(false, true);
    var visibleControl = Knot.Converters.createBoolToValue("", "none");
    //these validators are used to validate the inputted value. the last one(duplicatedName) is used to check whether these's another one
    //with the same name. You can see how simple it is.
    var mustNotNull = Knot.Validators.createNotNull("Must not be null!");
    var checkInteger = Knot.Validators.createInteger("Must be integer");
    var ageRange = Knot.Validators.createValueRange(0, 150, "Must in 0~150!");
    var sexToTitle = Knot.Converters.createKeysToValues(["Male", "Female"], ["Mr.", "Miss."]);
    var duplicatedName = function (value, data) {
        for (var i = 0; i < model.selectedCategory.peopleList.length; i++) {
            if (model.selectedCategory.peopleList[i] == data)
                continue;
            if (model.selectedCategory.peopleList[i].name == value) {
                return value + " already exists!";
            }
        }
    }
    var boolToExpandSymbol = Knot.Converters.createBoolToValue("-", "+");
    var boolToOpacity = Knot.Converters.createBoolToValue(1, 0);
    //options for the "From" select
    var countries = ["USA", "Japan", "China", "Australia"];
 | 
 |