在自定义绑定中添加新标记

时间:2014-06-11 11:36:50

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

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

这是一个非常简化的例子looks like this。如您所见,您可以输入各种代码(tag1tag2,...,tag5),并且可观察的内容正在发生变化。所以我的自定义绑定适用于这种情况。

这是:

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"}
})

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

P.S。

  • 如果您认为我的约束力很差,我同意您的意见,并乐于听取如何改进它。

  • 如果您需要澄清有约束力的工作方式,我很乐意提供。

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

  • 如果你想改变许多你喜欢的东西

2 个答案:

答案 0 :(得分:2)

更新了答案,&#39;更正&#39;图案

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

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

首先,让我们看一下来自update的{​​{1}}。

valueAccessor()

这里我们为模型中的任何传入数据创建标记。所有标记,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()

删除令牌时,令牌字段(patched)中缺少来自我们的AJAX自动完成的额外元数据。因此,我们必须根据确实存在的属性进行查找:

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"

答案 1 :(得分:0)

这个答案是向后的

请参阅其他版本。


直接将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" 1}}或任何其他对象键。他们只有创建令牌的文本/标签。

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

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

removeItem()

这允许我创建没有前缀的令牌。我使用的是Couchbase NoSQL,因此这与数据/文档的存储方式非常吻合。

因此,id必须搜索数组,尝试匹配value或后退仅查找allBindingsAccessor()。可以改进此部分以接受来自init的绑定变量以控制匹配哪个字段。

现在在活页夹ko.utils.registerEventHandler(element, 'tokenfield:removedtoken', function (e) { console.log('tokenfield:removedtoken'); console.log(e); tokenBaseModel.fields[element.id].removeItem(e.attrs); }); 中,我们可以定义将响应令牌字段的eventHandlers。

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将被评估到我的tokenfield模型范围之外的外部字段valueAccessor()。 (valueHasMutated()实际上是一个获取/设置的函数,而不是直接指向该值的链接。

然后,最后点击fooModel.bar会触发更改以更新{{1}}绑定的其他元素。