【问题标题】:Add new tags in custom binding在自定义绑定中添加新标签
【发布时间】:2014-06-11 11:36:49
【问题描述】:

我正在使用带有bootstrap-tokenfieldtypeahead 的淘汰赛来显示标签。以前我需要一种方法来以一种很好的方式显示我的标签,因此我创建了一个自定义绑定。当标签列表没有变化并且只有选定的标签发生变化时,它的效果非常好。

所以一个非常简化的例子looks like this。如您所见,您可以键入各种标签(tag1tag2、...、tag5)并且 observable 正在发生变化。所以我的自定义绑定在这种情况下有效。

这里是:

ko.bindingHandlers.tags = {
    init: function(element, valueAccessor, allBindings) {
        var initializeTags = function(listOfTags, inputID, max){
            var tags = new Bloodhound({
                local: listOfTags,
                datumTokenizer: function(d) {return Bloodhound.tokenizers.whitespace(d.value);},
                queryTokenizer: Bloodhound.tokenizers.whitespace
            });
            tags.initialize();
            inputID.tokenfield({
                limit : max,
                typeahead: {source: tags.ttAdapter()}
            }).on('tokenfield:preparetoken', function (e) {
                var str = e.token.value,
                    flag = false,
                    i, l;
                for(i = 0, l = listOfTags.length; i < l; i++){
                    if (listOfTags[i]['value'] === str){
                        flag = true;
                        break;
                    }
                }

                if (!flag){
                    e.token = false;
                }
            });
        }

        var options = allBindings().tagsOptions,
            currentTagsList = Helper.tags1List,
            currentTagsInverted = Helper.tags1Inverted;

        initializeTags(currentTagsList, $(element), 4);

        ko.utils.registerEventHandler(element, "change", function () {
            var tags = $(element).tokenfield('getTokens'),
                tagsID = [],
                observable = valueAccessor(), i, l, tagID;

            for (i = 0, l = tags.length, tagID; i < l; i++){
                tagID = currentTagsInverted[tags[i].value];

                if (typeof tagID !== 'undefined'){
                    tagsID.push(parseInt(tagID));
                }
            }

            observable( tagsID );
        });
    },
    update: function(element, valueAccessor, allBindings) {
        var arr     = ko.utils.unwrapObservable(valueAccessor()),
            options = allBindings().tagsOptions,
            currentTags = Helper.tags1, tagsName = [], i, l, tagName;

        if ( !(arr instanceof Array) ){
            arr = [];
        }

        for (i = 0, l = arr.length, tagName; i < l; i++){
            tagName = currentTags[arr[i]];
            if (typeof tagName !== 'undefined'){
                tagsName.push(tagName);
            }

        }
        $(element).tokenfield('setTokens', tagsName);
    }
};

但问题是我需要添加额外的标签:tag6 如果我只是这样做

Helper.getAllTags({
    "1":{"value":"tag1"}, ..., "6":{"value":"tag6"}
})

它不起作用(这对我来说并不奇怪,我知道它为什么不起作用)。这样做的正确方法是什么。

附言

  • 如果您认为我的绑定很糟糕,我同意您的看法,并且很乐意听到如何改进它。

  • 如果您需要说明绑定的工作原理,我很乐意提供。

  • 拥有tags1, tags1List, tags1Inverted 的想法是能够通过 id 或名称快速找到合适的标签(我有 500 个这样的标签)。

  • 如果您想更改很多内容,欢迎您

【问题讨论】:

    标签: knockout.js custom-binding bootstrap-tokenfield


    【解决方案1】:

    更新答案,“正确”模式

    我已经为 bootstrap-tokenfield 创建了一个 KnockoutJS 绑定。

    https://github.com/mryellow/knockoutjs-tokenfield

    首先让我们看看来自valueAccessor()updates。

    update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var observable = valueAccessor() || { };
        var peeked = ko.unwrap(observable.peek());
    
        ko.tokenfield[element.id].handlerEnabled = false;
    
        $(element).tokenfield('setTokens',peeked);
    
        ko.tokenfield[element.id].handlerEnabled = true;
    }
    

    在这里,我们为来自模型的任何传入数据创建令牌。所有的标记,如valueAccessor() 为我们提供了完整的对象。然而,这将触发tokenfield:createdtoken,它位于我们绑定的init 一侧。因此,为了避免将这些令牌重新保存到模型中,我们设置了一个变量 handlerEnabled 来控制事件流。

    现在对于任何用户交互、HTML value 属性或模型更改都会触发此事件:

    ko.utils.registerEventHandler(element, 'tokenfield:createdtoken', function (e) {
        // Detect private token created.
        if (e.attrs[ko.tokenfield[element.id].bindings['KeyDisplay']].indexOf("_") === 0) {
            console.log('tt-private');
            $(e.relatedTarget).addClass('tt-private');
        }
    
        // Allow `update` to temporarily disable pushing back when this event fires.
        if (ko.tokenfield[element.id].handlerEnabled == true) observable.push(e.attrs);
    
    });
    

    注意handlerEnabled 全局以阻止重新添加到valueAccessor()

    删除令牌时,来自我们的 AJAX 自动完成功能的额外元数据会从令牌字段 (patched) 中丢失。因此,我们必须根据确实存在的属性来查找它:

    ko.utils.registerEventHandler(element, 'tokenfield:removedtoken', function (e) {
        var peeked = observable.peek();
        var item;
        // Find item using tokenfield default values, other values are not in tokenfield meta-data.
        ko.utils.arrayForEach(peeked, function(x) {
            if (ko.unwrap(x.label) === e.attrs.label && ko.unwrap(x.value) === e.attrs.value) item = x;
        });
    
        observable.remove(item); // Validation of `item` likely needed
    });
    

    这样就覆盖了活页夹的内部。现在我们按照 KnockoutJS 的预期将所有内容直接保存到绑定模型中,而不会出现数据重复或同步问题。让我们返回那个 CSV 字段,使用返回一个计算值的 observableArray.fn 很好并且可重用。

    用法:self.tags_csv = self.tags.computeCsv();

    ko.observableArray['fn'].computeCsv = function() {
        console.log('observableArray.computeCsv');
        var self = this;        
    
        return ko.computed({
            read: function () {
                console.log('computed.read');
    
                var csv = '';
                ko.utils.arrayForEach(ko.unwrap(self), function(item) {
                    console.log('item:'+JSON.stringify(item));
                    if (csv != '') csv += ',';
                    // Our ID from AJAX response.
                    if (item.id !== undefined) {
                        csv += item.id;
                    // Tokenfield's ID form `value` attrs.
                    } else if (item.value !== undefined) {
                        csv += item.value;
                    // The label, no ID available.
                    } else {
                        csv += item.label;
                    }                   
                });
    
                return csv;
            },
            write: function (value) {
                console.log('computed.write');
    
                ko.utils.arrayForEach(value.split(','), function(item) {
                    self.push({
                        label: item,
                        value: item
                    });
                });
    
            }
        });
    };
    

    现在我们的模型中有一个对象数组和一个 CSV 表示,可以在发送到服务器之前进行映射或操作。

    "tags": [
        {
            "label": "tag1",
            "value": "tag1"
        },
        {
            "id": "id from AJAX",
            "field": "field from AJAX",
            "label": "tag2",
            "value": "tag2"
        }
    ],
    "tags_csv": "tag1,id from AJAX"
    

    【讨论】:

    • 似乎 tokenfield 确实在 token、id 等上存储了额外的元数据。
    【解决方案2】:

    这个答案是倒退的

    请参考其他版本。


    addItem()/removeItem() 直接添加到模型有助于使事情更易于管理。下面是我的模型,其中包含与每个字段关联的项目。

    var tokenFieldModel = function tokenFieldModel() {
        var self = this;
        this.items = ko.observableArray([]);
    
        this.addItem = function(attrs) {
            console.log('addItem');
            self.items.push(new tokenItemModel(attrs));
        };
    
        this.removeItem = function(attrs) {
            console.log('removeItem');
            var item;
            if (attrs.id != null) {
                ko.utils.arrayForEach(this.items(), function(x) {
                    if(x.id === attrs.id && ko.unwrap(x.value) == attrs.value) item = x;
                });
            } else {
                ko.utils.arrayForEach(this.items(), function(x) {
                    // TODO: Use allBindingsAccessor().tokenFieldDisplay
                    if(ko.unwrap(x.value) === attrs.value) item = x;
                });
            }
            //console.log(ko.unwrap(this.items()));
            self.items.remove(item);
        };
    };
    

    removeItem() 看起来很乱,必须循环处理,但对我的情况有点特殊,我想添加尚未与自动完成匹配且没有 id 或任何其他对象键的令牌.他们将只有创建令牌的文本/标签。

    然后我可以将其发送到服务器,如下所示:

    field_id = "id:111, id:222, a new tag, id:333, another new tag"
    

    field_id = [
        {
            id: 111,
            value: 'tag1',
            label: 'tag1'
        },
        {
            id: 222,
            value: 'tag2',
            label: 'tag2'
        },
        {
            value: 'a new tag'
        },
    ]
    

    这允许我创建没有前缀的标记。我使用的是 Couchbase NoSQL,所以这正好适合数据/文档的存储方式。

    因此,removeItem() 必须在数组中搜索以尝试匹配 id,或者返回仅寻找 value。这部分可以改进以接受来自allBindingsAccessor() 的绑定变量来控制匹配哪个字段。

    现在在 binder 的 init 中,我们可以定义将响应 tokenfield 的事件处理程序。

    ko.utils.registerEventHandler(element, 'tokenfield:removedtoken', function (e) {
        console.log('tokenfield:removedtoken');
        console.log(e);
    
        tokenBaseModel.fields[element.id].removeItem(e.attrs);
    });
    

    请注意,我将页面上的每个令牌字段放在一个数组 tokenBaseModel.fields() 中,并由它们的 elementId 索引(不是 obserableArray(),只是一个普通数组,用于存储页面上每个令牌字段的单独项目列表)。

    var tokenBaseModel = {
        fields: []
    };
    

    然后在绑定器update 部分中,我们可以将令牌字段中的值传递回data-bind 属性本身中定义的其他模型。

    update: function(element, valueAccessor, allBindingsAccessor, bindingContext) {
        console.log('update');
        var observable = valueAccessor() || {};
    
        // Does validation on allBindingsAccessor and sets defaults.
        var bindings = new tokenFieldUtils().processBindings(allBindingsAccessor);
    
        // An `fn` util function extending both `observableArray` and `observable` to accept whichever datatype they're expecting and sort it out.
        observable.refreshAll(ko.unwrap(tokenBaseModel.fields[element.id].items),bindings['Delimiter'],bindings['FieldKey']);
    
    }
    

    最后我的refreshAll()(实际上是valueAccessor()().refreshAll())函数完成了将数据传回valueAccessor()的繁重工作。

     ko.observableArray['fn'].refreshAll = function(valuesToPush, delimiter, key) {
        var underlyingArray = this();
        this.valueWillMutate();
        this.removeAll();
        ko.utils.arrayPushAll(underlyingArray, valuesToPush);
        this.valueHasMutated();
        return this;
    };
    
    ko.observable['fn'].refreshAll() = function(valuesToPush, delimiter, key) {
        this.valueWillMutate();
        var csv = '';
        ko.utils.arrayForEach(valuesToPush, function(item) {
            if (csv != '') csv += delimiter;
            if (item[key] === undefined) {
                csv += item['value'];
            } else {
                csv += item[key];
            }
        });
        this(csv);
        this.valueHasMutated();
        return this;
    };
    

    将绑定定义为data-bind="tokenfield: fooModel.bar" 意味着valueAccessor() 将被评估为外部字段fooModel.bar,这超出了我的令牌字段模型的范围。 (valueAccessor() 实际上是一个获取/设置的函数,而不是直接指向值的链接)。

    最后点击valueHasMutated() 会触发更改以更新绑定fooModel.bar 的其他元素。

    【讨论】:

    • 哇哦!分数!我加入了那些可以评论和自欺欺人的人。 ;-) 谢谢。
    • 注意使用 items 数组而不是 valueAccessor 的“更新”意味着它不能接受来自外部的值来创建新的令牌,完成 id 和其他字段。但是,它可以从 value 属性创建令牌,因为 tokenfield 会为这些触发创建令牌事件。使用双向解决方案,可能只需要支持数组并在另一端处理构建 CSV,因此始终存储所有键。
    • 好的,这是一种有趣的方法,但对于处理 KnockoutJS 来说并不“正确”。可以利用来自父模型的传入更新,首先使用peek 查看内容,而不会无意中订阅我们的items 数组。也可以覆盖notifySubscribers。然而,这很快会导致竞争条件,您正在追逐脏标志并需要将内部模型与外部模型同步。与所有这些变化的编组和不断重建 CSV 相比,使用外部模型和计算机可观察是更好的模式。
    • k 已排序,请接受新答案。原作留作历史,其中有一些技巧。
    猜你喜欢
    • 2021-02-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-10
    • 1970-01-01
    • 2013-02-25
    相关资源
    最近更新 更多