首页 > 解决方案 > 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.

标签: knockout.js

解决方案


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 :)


推荐阅读