Knockout: Change options in the picklist without changing the value in the view model

I have a JS knockout issue with some cascading option lists and disabling the main "active" object they refer to.

I created a jsfiddle to demonstrate this problem.

I have a user interface where users edit the main header record and add / remove / edit child records. There is a central area for editing child records. The idea is to click on a child record in the table and it will become an editable record in the middle area.

The problem I have is that the list of things in the second dropdown changes depending on the first one. This is fine as long as the active entry does not change. If the category changes as the active entry changes, the "things" list also changes. At this point, the selected "thing" (2nd dropdown) in the new active child is cleared.

I accept the value in the new active changes to the entry, but clears up because it doesn't show up in the old list (if the category is changed). The list of items is then changed (including the corresponding value), but that value has already passed from the view model.

(I realize there is a rather long explanation, I hope the jsfiddle makes it clear)

How can I change the list of items in the dropdown AND the selected value in the view model without losing the selected value?

HTML:

<label>Some header field</label>
<input type="text" id="txtSomeHeaderField" data-bind="value: HeaderField" />

<fieldset>
    <legend>Active Child Record</legend>

    <label>Category</label>
    <select id="ddlCategory" data-bind="options: categories, value: ActiveChildRecord().Category, optionsCaption:'Select category...'" ></select>

    <label>Things</label>
    <select id="ddlThings" data-bind="options: ThingsList, value: ActiveChildRecord().Favorite, optionsCaption:'Select favorite thing...'" ></select>
</fieldset>

<button data-bind="click: AddChildRecord" >Add a child record</button>

<table id="tblChildRecords" border>
    <thead>
        <tr>
            <th>Category</th>
            <th>Favorite Thing</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: ChildRecords">
        <tr data-bind="click: ChildRecordClicked, css: {activeRow: ActiveChildRecord() === $data}" >
            <td data-bind="text: Category"></td>
            <td data-bind="text: Favorite"></td>
        </tr>
    </tbody>
</table>

<p>Steps to reproduce problem:</p>
<ol>
    <li>Click "Add child record"</li>
    <li>Click on that row to make it the "active" record</li>
    <li>Choose category "Pets" and thing "Dog"</li>
    <li>Click "Add child record"</li>
    <li>Click on the new row to make it the "active" record</li>
    <li>Choose category "Colours" and thing "Blue"</li>
    <li>Now click back on the first row... <strong>"Dog" disappears!</strong></li>
</ol>

      

JavaScript:

var categories = ["Pets", "Colours", "Foods"];

var MyViewModel = function(){
    var _this = this;

    this.HeaderField = ko.observable("this value is unimportant");
    this.ChildRecords = ko.observableArray([]);
    this.ActiveChildRecord = ko.observable({ Category: ko.observable("-"), Favorite: ko.observable("-")});    
    this.ThingsList = ko.observableArray();

    this.AddChildRecord = function(){
        _this.ChildRecords.push({ Category: ko.observable("-"), Favorite: ko.observable("-")});
    }

    this.ChildRecordClicked = function(childRecord){
        _this.ActiveChildRecord(childRecord);
    }

    this.RefreshThingsList = ko.computed(function(){
        var strActiveCategory = _this.ActiveChildRecord().Category();
        switch(strActiveCategory){
            case "Pets": _this.ThingsList(["Dog", "Cat", "Fish"]); break;      
            case "Colours": _this.ThingsList(["Red", "Green", "Blue", "Orange"]); break;
            case "Foods": _this.ThingsList(["Apple", "Orange", "Strawberry"]); break;
        }        

    });
}

ko.applyBindings(MyViewModel);

      

+3


source to share


3 answers


Knockout value. Binding to the AllowUnset attribute could be cleaner.

http://jsfiddle.net/5mpwx501/8/

<select id="ddlCategory" data-bind="options: categories, value: ActiveChildRecord().Category, valueAllowUnset: true, optionsCaption:'Select category...'" ></select>
<select id="ddlThings" data-bind="options: ThingsList, value: ActiveChildRecord().Favorite, valueAllowUnset: true, optionsCaption:'Select favorite thing...'" ></select>

      



@super cool is 100% correct, but the reason it is undefined is because the ActiveChildRecord changes when you hit the pet line, but this computational function hasn't been executed yet, so you got a small timeframe where Dog is the favorite, but the options are still Colors . Since Dog is not an option, the dropdown will set the Favorite property on ActiveChildRecord to undefined.

I would use the valueAllowUnset binding. Basically it tells the dropdown menu that if there is no match then don't set the value to undefined, but rather wait, because the parameters might update.

A nice side effect of using this binding is that when a new child is added, it does not copy the previous line. This naturally resets the choice for you.

+5


source


I took a completely different approach, using update list subscriptions and values, and a dedicated observable to store the edited record.

<fieldset>
    <legend>Active Child Record</legend>
    <label>Category</label>
    <select id="ddlCategory" 
       data-bind="options: categories, value: category, 
                  optionsCaption:'Select category...'" ></select>
    <label>Things</label>
    <select id="ddlThings" 
       data-bind="options: things, value: thing, 
                  optionsCaption:'Select favorite thing...'" ></select>
</fieldset>

<button data-bind="click: AddChildRecord" >Add a child record</button>

<table id="tblChildRecords" border>
    <thead>
        <tr>
            <th>Category</th>
            <th>Favorite Thing</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: childRecords">
        <tr data-bind="click: ChildRecordClicked, 
                css: {activeRow: editedRecord() === $data}" >
            <td data-bind="text: category"></td>
            <td data-bind="text: thing"></td>
        </tr>
    </tbody>
</table>

      

JavaScript:

var categories = ["Pets", "Colours", "Foods"];

var MyViewModel = function(){
    var _this = this;

    this.categories = ko.observableArray(["Pets","Colours","Foods"]);
    this.category = ko.observable();
    this.category.subscribe(function(newCategory){
        _this.refreshThings(newCategory);
        if(editedRecord()) {
            editedRecord().category(newCategory);
        }
    });

    this.things = ko.observableArray([]);
    this.thing = ko.observable();
    _this.refreshThings = function(newCategory){
        switch(newCategory){
            case "Pets": _this.things(["Dog", "Cat", "Fish"]); break;      
            case "Colours": _this.things(["Red", "Green", "Blue", "Orange"]); break;
            case "Foods": _this.things(["Apple", "Orange", "Strawberry"]); break;
        }        
    };
    this.thing.subscribe(function(newThing){
        if(editedRecord()) {
            editedRecord().thing(newThing);
        }
    });

    this.childRecords = ko.observableArray([]);
    this.editedRecord = ko.observable();

    this.AddChildRecord = function(){
        var newRecord = {
            category: ko.observable(),
            thing: ko.observable()
        };
        _this.childRecords.push(newRecord);
        _this.editedRecord(newRecord);
        _this.category('');
        _this.thing('');
    }

    this.ChildRecordClicked = function(childRecord){
        _this.editedRecord(null);
        _this.category(childRecord.category())
        _this.thing(childRecord.thing())
        _this.editedRecord(childRecord);
    }    

}

ko.applyBindings(MyViewModel);

      

A few notes:

  • a new observable called 'editRecord' is used. This can hold the value of the currently edited record (either new or selected by clicking it), or a null value if nothing needs to be edited (this value is set to AddChildRecord

    and ChildrecordClicked

    ) to change the changes while the lists are updated)
  • there is an array categories

    , a category

    observable and a subscription that updates the list of things, and also the category property of the edited entry, if present
  • there is an array things

    , an thing

    observable, and a subscription that updates the thing property of the edited record if present
  • AddChildRecord

    , creates a new, empty entry and sets it as an edited entry. Apart from initializing both lists of categories and things
  • childRecordClick

    sets a recorded recording as an edited recording


As you can see, with this technique, the bindings remain very simple and you have complete control over what happens at any moment.

You can use methods like this to allow unpublishing and the like. In fact, I usually edit the post elsewhere and add it or apply the changes after the user accepts them, allowing them to undo.

This is your modified violin .

Finally, if you want to preserve forward slashes in unedited entries, make this change:

this.AddChildRecord = function(){
    _this.editedRecord(null);
    var newRecord = {
        category: ko.observable("-"),
        thing: ko.observable("-")
    };
    _this.childRecords.push(newRecord);
    _this.category('');
    _this.thing('');
    _this.editedRecord(newRecord);
}

      

included in this version of the script , but it would be better if you style the table cell to have a minimum height and keep it blank as in the previous version.

+2


source


I made a small modification to your fiddle which worked great.

Show model:

this.RefreshThingsList = ko.computed(function(){
        var store= ActiveChildRecord().Favorite();
        var strActiveCategory = _this.ActiveChildRecord().Category();
        switch(strActiveCategory){
            case "Pets": _this.ThingsList(["Dog", "Cat", "Fish"]); break;      
            case "Colours": _this.ThingsList(["Red", "Green", "Blue", "Orange"]); break;
            case "Foods": _this.ThingsList(["Apple", "Orange", "Strawberry"]); break;
        }      
        alert(ActiveChildRecord().Favorite()); // debug here you get undefined on your step 7 so store the value upfront and use it .
       ActiveChildRecord().Favorite(store);
    });

      

working violin here

Just in case you're looking for something other than let us know.

+1


source







All Articles