3

A typical scenario in our environment is to allow the user to select a list of options provided by the server (terminals, products, ..), and then present the requested data.

The options provided by the server is in Name, ID format, and thus the following knockout construct is used all to often:

<select data-bind="options: serverOptions, optionsText: 'Name', optionsValue: 'ID', value: selectedOption>

It would be desirable to make a custom bindingHandler, called 'NamedIdOptions' specifying optionsText and optionsValue on the allBindingsAccessor() and then redirect to the standard options binding handler.

i.e.

<select data-bind="NamedIdOptions: serverOptions, value: selectedOption"></select>

Previously, I have made own binding handler which fills in the options my self - however, I would prefer to use the framework provided by the options binding handler.

I have tried different approaches without too much success - the options binding uses allBindings['optionsValue'], and allBindings['optionsText'] to access the value, and it seems that I have no way of setting these. (I would like to avoid applyBindingsToNode approach used in and write something along the lines of:

ko.bindingHandlers.NamedIdOptions = {
    init: function(element, valueAccessor, allBindingsAccessor, viewModel)
    {
        var allBindings = allBindingsAccessor();
        allBindings.*FORCESET*("optionsText", "Name");
        allBindings.*FORCESET*("optionsValue", "ID");

        retun ko.bindingHandlers.options.init.apply(this, arguments);
    },
    update: function (element, valueAccessor, allBindingsAccessor, viewModel)
    {
        retun ko.bindingHandlers.options.update.apply(this, arguments);
    }
}

However, it seems like I have no possiblity to set anything on the allBindings.

I am not allowed to use

allBindings['_ko_property_writers']['optionsText']("Name" );
allBindings['_ko_property_writers']['optionsValue']("ID" );

I would really prefer to avoid applyBindingsToNode in the init construct as

Knockout - is it possible to combine standard select bindings with a custom binding?

Does anyone now about a simple solution to the problem?

Community
  • 1
  • 1
Jesper Kleis
  • 51
  • 1
  • 6
  • I don't know if this exactly answers your question, but if you are creating a re-usable control you can always use computedOptions and computedName and have those be observables that are set to the value of whatever is passed in. – PW Kad Nov 15 '13 at 14:19
  • In older Knockout versions you used to be able to get away with adding optionsText and optionsValue straight to allBindings and it would work. That's what I was doing for exactly the same scenario as you've described. – BrandonLWhite Feb 06 '14 at 03:22

4 Answers4

3

You might consider using ko.applyBindingAccessorsToNode. This is how I've started doing it in KO 3.0:

ko.bindingHandlers.NamedIdOptions = {
    init: function(element, valueAccessor, allBindingsAccessor)
    {
        var injectedBindingValues = {        
            options: valueAccessor,
            optionsValue: function () { return "ID" },
            optionsText: function () { return "Name" }
        };

        ko.applyBindingAccessorsToNode(element, injectedBindingValues);

        //tell Knockout that we have already handled binding the children of this element
        //
        return { controlsDescendantBindings: true };        
    }
}

You can see it in action in this fiddle.

Note: I typically use JSON schema sent from the server (C#, JSON.NET) to automate populating options in the UI from C# attributes or DB schema metadata. I distilled my code and changed it to match what the OP's doing for continuity with the question. But if there is any interest in the JSON schema technique hit me up and I can post it.

BrandonLWhite
  • 1,866
  • 1
  • 23
  • 26
  • Thx alot - apparently, it is not defined in KO 2.2 we use in our production environment. However, the solution is much cleaner than mine above. – Jesper Kleis Feb 17 '14 at 11:39
  • About the server connection - We keep our C# view models as tiny as possible, and automatically serialize them in Json format, and convert them to javasscript objects. Thus, typically we would write something along the lines: ` – Jesper Kleis Feb 17 '14 at 11:42
2

Okay - I ended up using apply bindings to node anyway:

ko.bindingHandlers.NamedIdOptions =
{
    init: function (element, valueAccessor, allBindingsAccessor, viewModel)
    {
       var allBindings = allBindingsAccessor();
       var newBindingOptions = { options: allBindings.NamedIdOptions, optionsText: "Name", optionsValue: "ID" };

       delete allBindings.NamedIdOptions;
       ko.utils.extend(newBindingOptions, allBindings);

       ko.applyBindingsToNode(element, newBindingOptions, viewModel);
    }
};

And it seems to work out as expected - I am a bit unsure about the value and selectedOptions - which have 'after' set to options. I guess I am safe when the NamedIdOptions is plaved before the value binding?

Jesper Kleis
  • 51
  • 1
  • 6
  • There can be problems only deleting the NamedIdOptions - actually, all the actions already invoked should be deleted first – Jesper Kleis Feb 19 '14 at 07:24
1

Can't you just fake the whole allBindingsAccessor parameter when forwarding the call?

update: function (element, valueAccessor, allBindingsAccessor, viewModel)
{
    var allBindings = allBindingsAccessor(),
        fakeAllBindingsAccessor = function () {
            // I've used jQuery.extend here, you could also manually add the properties to the allBindings object
            return $.extend(true, allBindings, {
                optionsValue: 'ID',
                optionsText: 'Name'
            };
        };
    return ko.bindingHandlers.options.init.call(this, element, valueAccessor, fakeAllBindingsAccessor, viewModel);
}

Edit: added some more code to combine the existing allBindingsAccessor with the manual fake bindings

Hans Roerdinkholder
  • 3,000
  • 1
  • 20
  • 30
  • That is definitely worth a try – Jesper Kleis Nov 15 '13 at 15:28
  • Unfortunately, it does not seem to work. It is possible to make a fake allbindingsaccesor in the init function. At it works - however, the fake override is overridden for the update. If one does the same trick in the update method, the following one gets the following error: _'undefined' is not a function (evaluating 'd.get("optionsIncludeDestroyed")')_ – Jesper Kleis Nov 16 '13 at 21:15
  • Can you post the full data-bind of your HTML tag? I will debug and see if I can spot my mistake. – Hans Roerdinkholder Nov 17 '13 at 22:32
  • The basic problem, is the the allBindingHandler is not a function - but an object with a get method for its options (but no set function). Thus it seems that it is not that simple to make a fake allBindingsAccessor – Jesper Kleis Nov 19 '13 at 14:37
0

I ended up with the following solution, which also allows to make simple dependendt filters - it uses underscore for that, but that is just a matter of convenience:

// NamedIdOptions - is basically equal to the options binding - except, optionsText="Name", and "optionsValue='ID'"
// The options can be filered - Specifying optionsFilter: {'FilterProp' : 'valueToBeMatched', 'FilterProp2' : VMpropToMatch, .. }
// Definig optionsFilterCallback, registers a callback which will be invoked with the matched elements
// which can be used to turn off elements etc.
ko.bindingHandlers.NamedIdOptions =
{
init: function (element, valueAccessor, allBindingsAccessor, viewModel)
{
    var allBindings = allBindingsAccessor(),
        appliedValueAccesor = valueAccessor(),
        shownOptions = appliedValueAccesor,
        unwrap = ko.utils.unwrapObservable;

    if (allBindings.optionsFilter)
    {
        shownOptions = ko.computed(function ()
        {
            // First  - find all items to be presented in the list
            var allItems = unwrap(appliedValueAccesor);

            // Extract items to match against
            // it is ensured that the computed observable dependts on all its sub properties
            // All items are matched by key into an array 
            // if the match is null, undefined, or an empty array, it is not included int the match
            var matchItems = {};
            _.each(_.keys(allBindings.optionsFilter), function (key)
            {
                var observedValues = unwrap(allBindings.optionsFilter[key]);
                if (!observedValues)
                    return;

                if (!_.isArray(observedValues))
                    observedValues = [observedValues];

                matchItems[key] = observedValues;
            });

            // Find items that match the items above - uses ko own routine to do so
            var matchedItems = _.filter(allItems, function (elm)
            {
                return _.all(_.keys(matchItems), function (key)
                {
                    var match = _.contains(matchItems[key], elm[key]);
                    return match;
                });
            });

            // if a callback is defined - call it with the matched items
            if (allBindings.optionsFilterCallback)
                allBindings.optionsFilterCallback(matchedItems);

            return matchedItems;
        }, this);
    }

    // Change the binding options - the already handled items should not be reapplied to the node
    // NOTE: it would be preferable to use 'ko.3.0->applyBindingAccessorsToNode' instead of the hack below
    // It assumes that the order of dictionaries are not changed - it works, but is not complient with the Ecmascript standard
    var newBindingOptions = { options: shownOptions, optionsText: "Name", optionsValue: "ID" };
    var bindingKeys = _.keys(allBindings);
    var handledItems = _.first(bindingKeys, _.indexOf(bindingKeys, "NamedIdOptions") + 1);
    _.each(handledItems, function (item)
    {
        delete allBindings[item];
    });

    _.extend(newBindingOptions, allBindings);

    ko.applyBindingsToNode(element, newBindingOptions, viewModel);
}
};
Jesper Kleis
  • 51
  • 1
  • 6