1

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

所以一个真正简化的例子看起来像这样。如您所见,您可以键入各种标签(tag1, tag2, ..., 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 个)。

  • 如果你想改变很多事情,欢迎你

4

2 回答 2

2

更新答案,“正确”模式

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

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

首先让我们看看updates 从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;
}

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

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

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 自动完成的额外元数据从令牌字段(已修补)中丢失。因此,我们必须根据确实存在的属性来查找它:

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返回一个计算值的 a 很好且可重用。

用法: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"
于 2014-07-27T00:19:42.590 回答
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"

或者

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

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

因此removeItem()必须搜索试图匹配的数组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);
});

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

var tokenBaseModel = {
    fields: []
};

然后在 bindersupdate部分,我们可以将 tokenfield 中的值传递回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绑定的其他元素。

于 2014-07-25T01:31:38.560 回答