Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with select element binding using options, optionsValue, and value #1269

Closed
loesak opened this issue Jan 3, 2014 · 27 comments
Closed

Comments

@loesak
Copy link

loesak commented Jan 3, 2014

Knockout 3.0.0

Assume the following:

I have a complex object (lets say Person) who has a reference variable (favoritePet) to another complex object (Pet). The value that can be held for the 'favoritePet' references comes from an asynchronous HTTP request. Meaning, if a value is populated in the 'favoritePet' reference, then the object selected is equivalent to another object in the fetched Pet instances, but is not the same object in memory.

According to the documentation, I need to provide the binding for 'optionsValue' and I assumed this would work:

<select class="form-control" data-bind="{
        'options' : availablePets,
        'optionsText' : function(item) { 
            return item.name; 
        },
        'optionsValue' : function(item) { 
            return item.id; 
        },
        'value' : favoritePet,
        'optionsCaption' : 'Choose ...'
    }"></select>

However, I found that it does the following:

  • when selecting a value, tries to assign the the selected pets 'id' to the reference variable instead of the pet instance itself
  • when a pet instance is already assigned to the reference variable, does not show that value initially selected when the view is bound

I dug through the code and I believe this is because the code upon section, tries to determine the data object that was selected by comparing the option elements value with the list of available data objects and upon setting selection, tries to compare the elements option text with the observable value. So in both instances trying to compare the option elements value (a string) with the object (an object), which will never be equal.

I can resolve the second problem by changing the data-binding to:

<select class="form-control" data-bind="{
        'options' : availablePets,
        'optionsText' : function(item) { 
            return item.name; 
        },
        'optionsValue' : function(item) { 
            return item.id; 
        },
        'value' : favoritePet().id,
        'optionsCaption' : 'Choose ...'
    }"></select>

Now when a value is assigned to the reference variable, then it can properly resolve which option to select by default because the assigned value for the binding is the observable values 'id' field, which eventually is equivalent to an the 'id' field of one of the instances of available pets.

However, this causes a different problem when there is no value assigned to the 'favoritePet' reference variable as 'favoritePet()' resolves to null, throwing an error.

I believe all this is again due to the code trying to determine the appropriate model to both select and assign by checking the equivalence of the value and the 'optionsValue' of each available data object.

Instead i think the code should be checking for equivalency between the values after begin passed through the 'optionsValue' method. Meaning this binding is valid:

<select class="form-control" data-bind="{
        'options' : availablePets,
        'optionsText' : function(item) { 
            return item.name; 
        },
        'optionsValue' : function(item) { 
            return item.id; 
        },
        'value' : favoritePet,
        'optionsCaption' : 'Choose ...'
    }"></select>

and equivalency is done by calling the 'optionsValue' function on BOTH the assigned value and the data object being compared.

Or Knockout should provide a means of letting the user define an equivalency method

<select class="form-control" data-bind="{
        'options' : availablePets,
        'optionsText' : function(item) { 
            return item.name; 
        },
        'optionsValue' : function(item) { 
            return item.id; 
        },
        'value' : favoritePet,
        'optionComparator' : function(val1, val2) {
            return val1.id === val2.id;
        }.
        'optionsCaption' : 'Choose ...'
    }"></select>
@loesak
Copy link
Author

loesak commented Jan 6, 2014

@mxlandry
Copy link

Is there any solution for that problem ? I came across the exact same issue.

@magic-uyr
Copy link

same issue here: person model has a title object and I am able to set the value of the title, however I am unable to select the selected value for a current Title when in edit form mode. really annoying when your working with databases like ravendb and you would store the full object, rather than a lookup reference.

@brianmhunt
Copy link
Member

I agree that a custom comparison function could be handy.

Also, not sure if this might help: Foreign Key Extender.

@dcroe
Copy link

dcroe commented Nov 19, 2014

hi,
is this only a problem of 3.x Versions?
It looks like we have the same problem in 3.0/3.1/3.2 but not in 2.1/2.2/2.3
for us this is a showstopper and prevents us from using 3.x versions ...
i don't understand why this is rated 'minor'- severity and only as feature.
imho this is a bug and should be fixed soon!

@magic-uyr
Copy link

I think I have found a solution for this, I use the following code to fetch in the object from my observable when I am binding / building my model. The changes should be quick and easy to make and works like a charm

item to enable search

       ko.observableArray.fn.find = function (value) {
            if (value !== null) {
                return ko.utils.arrayFirst(this(), function(item) {
                    return item.Id == value.Id;
                });
            } else {
                return false;
            }
          };

my original model, with broken select values

       var itemModel = function(item) {
        this.Id = ko.observable();
        this.DocumentElement = ko.observable();
        this.DocumentColumnId = ko.observable();
        this.OrderSequence = ko.observable();
        this.Required = ko.observable(false);
        this.ErrorMessage = ko.observable();
        this.FormType = ko.observable();
        this.Label = ko.observable();
        this.OptionType = ko.observable();
        this.InvocableType = ko.observable();
        this.PlaceholderText = ko.observable();

        var self = this;

        this.initialize = function(data) {
            self.Id(data.Id);
           ///  notice the code here,  observableitems.Find(exisiting value);
            self.DocumentElement(currentDoc().DocumentElements.find(data.DocumentElement));
            self.DocumentColumnId(data.DocumentColumnId);
            self.OrderSequence(data.OrderSequence);
            self.Required(data.Required);
            self.ErrorMessage(data.ErrorMessage);
            ///  notice the code here,  currentElement.Find(exisiting value);
            self.FormType(formTypes.find(data.FormType));
            self.Label(data.Label);
            self.OptionType(optionTypes.find(data.OptionType));
            ///  notice the code here,  currentElement.Find(exisiting value);
            self.InvocableType(invocableTypes.find(data.InvocableType));
            self.PlaceholderText(data.PlaceholderText);
        };

        self.initialize(item);

        this.cancel = function() {
            //cancel code (clears objects / observables )
        };

        this.updateItem = function () {
            //code to update / push
        };

        this.delete = function() {
            // delete code
        };

        this.createItem = function () {
            //code to save in db
        };
    };
    invocableTypes = ko.observableArray([{},{},{},{},{}]);  // some values / individual objects

my binding

<div class="row" data-bind="visible: currentItem, with: currentItem" id="additem">
    <div class="col-md-12">
        <h2>Create / Edit Item</h2>
        <div class="form-group">
            <label>Order Sequence</label>
            <input type="text" class="form-control" data-bind="value:OrderSequence" />
            <br />
            <label>Choose PDF item</label>
            <select data-bind="options: $root.currentDoc().DocumentElements, optionsText:'ElementId', value:DocumentElement, optionsCaption: 'Choose Item'"></select>
            <br />
            <label>Required Value</label>
            <input type="checkbox" data-bind="checked:Required" style="float:left; margin-left: 5px;" />
            <br class="clear clearfix" />
            <label>Form Input Type</label>
            <select data-bind="options: $root.formTypes, optionsText:'Name', value:FormType, optionsCaption: 'Choose Type'"></select>
            <br />
            <label>Label</label>
            <input type="text" class="form-control" data-bind="value:Label" />
            <br />
            <label>Options Type</label>
            <select data-bind="options: $root.optionTypes, optionsText:'Name', value:OptionType, optionsCaption: 'N/A'"></select>
            <br />
            <label>Dynamic Type</label>
            <select data-bind="options: $root.invocableTypes, optionsText:'Name', value:InvocableType, optionsCaption: 'Choose N/A'"></select>
            <br />
            <label>PlaceHolder (or dropdown first)</label>
            <input type="text" class="form-control" data-bind="value:PlaceholderText" />
            <br />
            <label>Error Message ( for edit )</label>
            <input type="text" class="form-control" data-bind="value:ErrorMessage" />
            <br />
            <br />
            <!-- ko if:Id -->
            <a class="btn btn-success" data-bind="click:updateItem">Update</a>
            <!-- /ko -->
            <!-- ko ifnot: Id -->
            <a class="btn btn-success" data-bind="click:createItem">Create</a>
            <!-- /ko -->
            &nbsp;
            <a class="btn btn-danger" data-bind="click:cancel">Cancel</a>
        </div>
    </div>
</div>

It took me a while to figure this all out. The problem here is that when you have a copy of an object
that is identical to your arrayObject it is still not regarded as a match. But by swapping your value, for the matching value in the Array, you are then matching the correct item within memory, and the scope of knockout and its ability to identify observables.

The above example is missing the base model currentDoc, however this is not needed to explain how the code works, this knockout allows for the creation of forms.

Hope this helps, works for me

Dave

@mbest
Copy link
Member

mbest commented Nov 19, 2014

@dcroe, If you can put together an example that works in 2.x but not in 3.x, that would help us determine whether this is a bug.

@dcroe
Copy link

dcroe commented Nov 20, 2014

hi,
i've tried to setup a simple jsfiddle. It uses the optionValue: 'key' version and it works with either 2.1 and 3.2. After that i have debugged the 'production' code:
it looks like the problem is in setting the value of the observable of the selected item.
this is the stacktrace:

observable()knocko...ebug.js (Zeile 1230)
writeValueToProperty(property=observable(), allBindings=allBindings(), key="value", value="OLDVALUE", checkIfDifferent=undefined)knocko...ebug.js (Zeile 2216)
valueUpdateHandler()knocko...ebug.js (Zeile 4318)
handle(c=Object { type="change", timeStamp=1416472365735, jQuery16201617626338694128=true, mehr...})jQuery-1.6.2.js (Zeile 17)
f(a=Object { type="change", timeStamp=1416472365735, jQuery16201617626338694128=true, mehr...})jQuery-1.6.2.js (Zeile 16)
f(c=Object { type="change", timeStamp=1416472365735, jQuery16201617626338694128=true, mehr...}, d=[Object { type="change", timeStamp=1416472365735, jQuery16201617626338694128=true, mehr...}], e=select#RC1565-select-box.text, g=undefined)jQuery-1.6.2.js (Zeile 17)
handle()jQuery-1.6.2.js (Zeile 17)
f(a=Object[select#RC1565-select-box.text], c=function(), d=undefined)jQuery-1.6.2.js (Zeile 16)
f(a=function(), b=undefined)jQuery-1.6.2.js (Zeile 16)
handle(a="change", b=undefined)jQuery-1.6.2.js (Zeile 17)
triggerEvent(element=select#RC1565-select-box.text, eventType="change")knocko...ebug.js (Zeile 388)
(?)()knocko...ebug.js (Zeile 4021)
ignore(callback=function(), callbackTarget=undefined, callbackArgs=undefined)knocko...ebug.js (Zeile 1194)
update(element=select#RC1565-select-box.text, valueAccessor=function(), allBindings=allBindings())knocko...ebug.js (Zeile 3998)
(?)()knocko...ebug.js (Zeile 2882)
evaluateImmediate(suppressChangeNotification=undefined)knocko...ebug.js (Zeile 1672)
evaluatePossiblyAsync()knocko...ebug.js (Zeile 1590)
notifySubscribers(valueToNotify="NEWVALUE", event="change")knocko...ebug.js (Zeile 1064)
valueHasMutated()knocko...ebug.js (Zeile 1243)
observable()knocko...ebug.js (Zeile 1228)

i'm not familiar with the internals of ko, but for me it looks like the "NEWVALUE" doesn't reach the valueUpdateHandler of the value binding, instead it uses the value of the valueAccessor() which hasn't been updated at that time ?

@mbest
Copy link
Member

mbest commented Nov 20, 2014

Can you provide a link to the jsfiddle? Also you say that it works in 2.1 and 3.2. Is that correct or do you mean 2.2?

@dcroe
Copy link

dcroe commented Nov 20, 2014

this is the version which works in 2.1 and 3.2, but the selected value is only the name and not an object:
http://jsfiddle.net/dcroe/y3mge8L7/5/

@dcroe
Copy link

dcroe commented Nov 20, 2014

in 2.1 the dependency tracking does not fire a change event which causes the newvalue to be lost...

@magic-uyr
Copy link

@mbest Hi, i you want to replicate this, use my code and remove the find. I think normally you only have this issue if your using a nosql style of document storage. With SQL / Rational, as you know, you would just reference an object with an int or string, but with nosql you would just store the entire object.

for example:     var Titles = ko.observableArray([{Id:1, Value:'Mr'},{Id:1, Value:'Ms'},{Id:1, Value:'Miss'},{Id:1, Value:'Mrs'}]);

var personModel = function(){
this.Id = ko.observable();
this.Name = ko.observable();
this.Title = ko.observable();   // in rational, this would just be the value of Id
// but as a document, we will keep the entire object from Titles.
};

html binding and the error

<!-- image here that we are With: personModel -->
<select data-bind="
options: $root.Titles, 
optionsText:'Value', 
value:Title <!-- this being personModel .Title -->, optionsCaption: 'Choose Title'">
</select>

Value in this instance is never selected, no matter what person.Title is equal to
because the title in array Titles is never match to the Title in person, even though they are the same object.

Hence my code above, that takes the actual title from titles and updates the person. Value is the same, there are no changes here, just KO never recognises a Title unless it is from the Titles Array.

@dcroe
Copy link

dcroe commented Nov 20, 2014

@magic-uyr: that is what i mean it works with optionsValue: 'Id' , but than you have only the id and not the object have a look at:
http://jsfiddle.net/dcroe/oej751L8/
but somehow it doesn't work in productional code , when setting the "Title" observable it will be overwritten by change event-> and it's original value retrieved by the valueAccessor

@dcroe
Copy link

dcroe commented Nov 24, 2014

after further investigation it looks like adding this option: valueAllowUnset=true will fix my problem.
i think in my case it has to do with the dynamic optionsCaption, which will show "please choose.." only if not allready chosen something. looks like the situation seems to be a bit more complex. I can't create a small jsfiddle to show the wrong behaviour.
Well, to add valueAllowUnset=true seems to work in 2.x and 3.x .

@bingxie
Copy link

bingxie commented Nov 28, 2014

I have the same issue, it's very annoying.

@phoeson
Copy link

phoeson commented Mar 17, 2015

why it gives me an undefined after bind the value
data-bind="options: branchList, optionsText: 'name', optionsValue: 'value', value: anything"
anything here will always be undefined after binding, why?

@mbest
Copy link
Member

mbest commented Mar 17, 2015

@kevinpan - Perhaps you need to add the option, valueAllowUnset: true. See http://knockoutjs.com/documentation/value-binding.html#using-valueallowunset-with-select-elements

@phoeson
Copy link

phoeson commented Mar 18, 2015

@mbest Thank You!

@chadbengen
Copy link

This binding handler is working for me (so far):

ko.bindingHandlers.valueByKey = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        var value = allBindings().value;
        var valueKey = value()[valueAccessor()];
        var match = ko.utils.arrayFirst(allBindings().options(), i => {
            var listItemKey = i[valueAccessor()];
            return listItemKey == valueKey;
        });
        allBindings().value(match);
    },
};
<select data-bind="options: $parent:countryTypes, value: country, valueByKey: 'id', optionsText:'countryName'></select>

Update:
BLEH! it doesnt appear to work if i have mulitple select's.

@asfernandes
Copy link

I do have the same problem. Something like optionsCompare: 'id' binding working with value and options would be good.

@mbest mbest added this to the 3.5.0+ milestone Nov 23, 2016
@asfernandes
Copy link

I submitted a pull request #2157

@asfernandes
Copy link

asfernandes commented Nov 28, 2016

Here is a less intrusive option which can be done without changing knockout core:

ko.bindingHandlers.objectSelect = {
    after: ['options', 'foreach'],

    init: function(element, valueAccessor, allBindings) {
        var interceptor = ko.computed({
            read: function() {
                var value = ko.utils.unwrapObservable(valueAccessor());
                var optionsValue = ko.utils.unwrapObservable(allBindings.get('optionsValue'));
                return value && value[optionsValue];
            },

            write: function(value) {
                var optionsValue = ko.utils.unwrapObservable(allBindings.get('optionsValue'));
                var options = ko.utils.unwrapObservable(allBindings.get('options'));

                var obj = ko.utils.arrayFirst(options, function(item) {
                    return item[optionsValue] == value;
                });

                valueAccessor()(obj);
            }
        });

        ko.applyBindingsToNode(element, {
            value: interceptor
        });
    }
};

Then replace value by objectSelect and continue using optionsValue.

@ZzZombo
Copy link

ZzZombo commented Jan 19, 2017

@asfernandes, that enables us to use objects as values of the SELECT element, do I understand this right?

@asfernandes
Copy link

@ZzZombo yeah.

@mbest
Copy link
Member

mbest commented Jul 19, 2017

This has been around a while, and thinking about this now, I'm pretty sure I want to keep the bindings as they are without adding a "comparer" option. Why?

  1. In Knockout, two objects that "look" the same can't actually be considered equivalent since they would likely have different observables.
  2. The current methods makes it clear that the there should only be one object representing each viewmodel. Borrowing from the OP example, assigning to the value initially would be done with something like ko.utils.arrayFirst(pets, function(p) { return p.id == "5678" });

@mbest mbest closed this as completed Jul 19, 2017
@mbest mbest modified the milestones: Not assigned, 3.5.0 Jul 19, 2017
@mbest mbest removed this from the Not assigned milestone Jul 19, 2017
@dcroe
Copy link

dcroe commented Jul 19, 2017

@mbest please consider following example:
let's create a simple example:
user has roles , within the system there are several thousand roles and User A has let's say only 200 of them.
The gui should manage the roles of the user with a select box.
if i understand you right , you propose to iterate over all thousand roles and replace the 200 user assigned roles with the the objects in the complete list?
what i think is missing here is an "entitymanager" which manages the instances, but for simplicity reasons i would realy like to see an identity comparison function.
the real question here is, how to keep object instances in sync between server and client if several calls to the server may return the same instance.

@mbest
Copy link
Member

mbest commented Jul 19, 2017

the real question here is, how to keep object instances in sync between server and client if several calls to the server may return the same instance.

Correct. And I don't think it should be the binding that does this.

if i understand you right , you propose to iterate over all thousand roles and replace the 200 user assigned roles with the the objects in the complete list?

If a select box is displaying 1000 values, iterating over those 1000 values is already happening.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests