Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Options binding for complex object selectedObject allways not in the list

Very often I have objects like this:

var objectToMap = {
    Id: 123, 
    UserType:{
        Id: 456,
        Name:"Some"
    }
};

When I need modify this object in user interface, I want to select from some list. For example, array:

var list = [
    {Id:456, Name: "Some"}, 
    {Id:567, Name: "Some other name"}];

I use options binding, something like that:

<select data-bind="options: list, optionsText: 'Name', value: UserType, optionsCaption: 'Select...'"></select>

Problem is, that knockout thinks that UserType from objectToMap {Id: 456, Name:"Some"} is different than object from list {Id:456, Name: "Some"}. So, automatically UserType gets to undefined, but not needed option from list.

I overcome problem this way: I find item in list using ko.utils.arrayFirst and replace UserType in objectToMap. But this looks ugly to me and requires additional coding. Is there better approach?

like image 642
VikciaR Avatar asked Apr 03 '14 05:04

VikciaR


2 Answers

Even though I think that the knockout behaviour is correct, as I mentioned in my comment, here you have a couple of ways to solve your problem (observe that all code below has error checking removed; you should, for example, handle cases where no item with the given id is found in the list, which the code below does not).

You could also create a custom bindingHandler which ensures that the property value is set to an item from a list property, based on some other form of equality (such as their JSON representation being equal). However, for me it feels as though this kind of logic (matching a property value with an item from a list property) fits better in the view model or potentially an extender, since these approaches would be easier to create unit tests for than involving bindingHandler logic for this.

Alternative 1 - Using optionsValue

Using the optionsValue binding you can set your select list to bind to the actual id. This would in many ways actually be the 'correct' way, since the objectToMap probably shouldn't contain the whole UserType and instead just contain the id. You would the add a computed property on the viewModel which gets the correct UserType based on the id.

self.myObject = {
    id: 9348,
    userTypeId: ko.observable(2)
};
//Add the userType computed property to the object
self.myObject.userType = ko.computed(function(){
    var id = self.myObject.userTypeId();
    return self.items.filter(function(item){
        return item.id === id;
    })[0];
});

You would then bind to the userTypeId property, like so:

<select data-bind="options:items, value: myObject.userTypeId, optionsValue: 'id', optionsText: 'name'">
</select>

This code can be tested in the jsfiddle at: http://jsfiddle.net/6Sg29/

Alternative 2 - Create a lookup-extender

If you really don't want to add another property to your object, you could create a one time lookup-extender which would lookup the value from the list, based on a comparison function which uses the id to find the correct item. Observe that this solution will hide mismatched data, if, for example, the name property differs between the item on the object and the item in the list.

An example of such an extender would be:

ko.extenders.oneTimeLookup = function(target, options) {
    var comparePropertyName = options.compare;
    var item = target();
    var foundItem = options.list.filter(function(listItem){
        return item[comparePropertyName] === listItem[comparePropertyName];
    })[0];
    target(foundItem);
    return target;
};

Where the extender would be used like the following:

self.myObject = {
    id: 9348,
    userType: ko.observable({
        id: 2,
        name: 'OriginalName'
    }).extend({ oneTimeLookup: { list: self.items, compare: 'id' } })
};

In this case, the binding can be just a plain options binding with the value pointing to the userType observable which has been extended, since we have performed the lookup

<select data-bind="options:items, value: myObject.userType, optionsText: 'name'">
</select>

This code can be tested running in a jsfiddle at http://jsfiddle.net/qGFQ7/

Alternative 3 - Do a manual lookup upon object creation.

This is the solution which you are already using, where you'd do a manual lookup and replace the object with an item from the list before doing any binding. This is similar to alternative 2, but less reusable. Which one more clearly states your intent you'll have to decide for yourself.

Summary

I'd suggest using the first approach, since the original object should probably not hold a reference to the full item anyway since you have a lookup list which you want to retrieve the item from. In the second and third approach you are just throwing away data which might hide potential bugs. But this is just my opinion on the matter in general and of course there might be specific cases where I would consider alternative 2 or 3.

like image 78
Robert Westerlund Avatar answered Sep 23 '22 03:09

Robert Westerlund


In my case, I had to keep the object clean without changing it.

It was a complex object like:

[
  {
    "scenario":{
        "id": "1",
        "name": "Scenario name"
    },
    "scenario_data: {}
  },
  {
    "scenario":{
        "id": "2",
        "name": "Scenario name"
    },
    "scenario_data: {}
  }
]

So I did this:

HTML

  <select data-bind="foreach:scenarios,event:{ change:Studio.selectScenario }">
     <option value="" data-bind="value:$data,text:$data.scenario.name"></option>
  </select>

JS

 self.selectScenario = function(obj,event){
    if (event.originalEvent) {
        self.currentScenario(Studio.currentSessionData().scenarios[event.originalEvent.srcElement.selectedIndex]);
    }
};
like image 24
Artipixel Avatar answered Sep 24 '22 03:09

Artipixel