knockout.js - Create custom Knockout binding that dynamically adds bound elements from filtered observable array
问题描述
I'm trying to create a custom KO binding that takes an observable array, and adds a nested element to the DOM to contain a filtered subset of the elements in the observable array.
On initialisation of my custom binding I think I need to do two things. Firstly extend the binding context adding a second observable array to hold a filtered subset of the observable array that this binds to. Secondly, add add the DOM elements I want after the bound element.
Then on update of the observable array that this binding binds to, populate the observable array added to the binding context during init.
So far I have the following, non working, vastly simplified experiment.
ko.bindingHandlers.suggester = {
init: function ( element, valueAccessor, allBindings, viewModel, bindingContext ) {
var innerBindingContext = bindingContext.extend(valueAccessor);
innerBindingContext.suggestions = ko.observableArray();
var ul_element = jQuery(
'<ul data-bind="foreach: suggestions">' +
'<li data-bind="text: suggestionText"></li>' +
'</ul>'
);
jQuery(element).after(ul_element);
ko.applyBindingsToDescendants(innerBindingContext, element);
return { controlsDescendantBindings: true };
},
update: function ( element, valueAccessor, allBindings, viewModel, bindingContext ) {
var self = this;
jQuery.each(ko.unwrap(valueAccessor), function (index,value) {
if (/*do some filtering*/) {
bindingContext.suggestions.push({suggestionText: value});
}
});
}
};
I'm well aware the above is very wrong, but I'm bouncing from one very wrong idea to the next and really need some help.
======== EDIT ========
I've been playing around and I have something near what I'm after, but which still doesn't work.
ko.bindingHandlers.autocomplete = {
init: function ( element, valueAccessor, allBindings, viewModel, bindingContext ) {
bindingContext.suggestions = ko.observableArray([{suggestionText: 'fred'}]);
var ul_element = jQuery(
'<ul data-bind="foreach: suggestions">' +
'<li data-bind="text: suggestionText"></li>' +
'</ul>'
);
jQuery(element).append(ul_element);
},
update: function ( element, valueAccessor, allBindings, viewModel, bindingContext ) {
var self = this;
bindingContext.suggestions.push({suggestionText: "another1"});
bindingContext.suggestions.push({suggestionText: "another2"});
}
};
This adds the observable array suggestions
to the binding context, adds the ul
/li
elements to the DOM and updates them correctly. The problem is that I want to add the <ul>
after the node I'm using this binding on, not within it. When I change jQuery(element).append(ul_element);
to jQuery(element).after(ul_element);
it doesn't work an nothing is displayed.
Additionally, I'm not sure if adding an observable directly to the binding context within my custom binding is the 'right' thing to do.
解决方案
Part of me wants to say: "If it works, it works", but I also feel you're slightly misusing a custom binding...
Custom bindings are generally used to do DOM modifications required for, for example, advanced user-interaction. For more advanced reusable patterns that combine UI code with viewmodels, there are knockout components.
You're using a custom binding as an advanced template
or foreach
binding, with a small chunk of custom behavior. Personally, I'd rewrite the custom binding in to a component. For example:
ko.components.register('suggestionWidget', {
viewModel: function(params) {
// Component requires two params:
// - suggestions: an (observable) array of "things"
// - filter: a (wrapped) filter function to go from
// `thing -> bool`
this.suggestions = ko.pureComputed(
() => ko.unwrap(params.suggestions)
.filter(ko.unwrap(params.filter))
);
},
template: `
<ul data-bind="foreach: suggestions">
<li data-bind="text: suggestionValue"></li>
</ul>`
});
const App = function() {
this.searchValue = ko.observable("");
this.filter = ko.pureComputed(() =>
this.searchValue()
? fruitSuggestion => fruitSuggestion
.suggestionValue
.includes(this.searchValue().toLowerCase())
: () => false
);
this.fruitSuggestions = ko.observableArray(
["apple", "banana", "orange", "mango", "pineapple"].map(suggestionValue => ({ suggestionValue }))
);
}
ko.applyBindings(new App());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<input type="text" data-bind="textInput: searchValue" placeholder="type 'apple' to get suggestions">
<div data-bind="component: {
name: 'suggestionWidget',
params: {
filter: filter,
suggestions: fruitSuggestions
}
}"></div>
If the suggestions and suggestion logic is "more generic", you could bake those in to the component. I chose to let the viewmodel provide both filter and content, but it's up to you.
This answer completely bypasses your current code, and I can imagine you'd want something a bit closer to that approach... However, you asked for some insights, so I thought I'd chip in with a completely different view :)
推荐阅读
- maps - 在 Power BI Maps 上绘制状态字符串
- java - Micronaut AWS Lambda 日志未显示在云端
- javascript - 我可以更改扩展类中方法的返回类型吗?
- cmake - 如何解决才能正确编译?
- python - Python中CSV列表的索引错误,即使在获得输出之后
- hadoop - Hive/Impala 列评论在几个字符后被截断
- computation-theory - 这台给定机器的“正则表达式”是什么?
- excel - 在 VBA 中跨 Subs(w/in 模块)定义变量
- apache-kafka - 将 Cloudera Kafka (CDK) 迁移到 Apache Kafka
- android - 从 RSS 提要读取数据 - getInputStream(); 碰撞