简体   繁体   中英

knockout observableArray remove causes errors and fails to remove

I have an issue that I have spent two days on trying to figure out, I will try to put everything here to explain the background without unnecessary information but please ask and I shall provide the info.

The problem

The process is, that the user selects the team they want to add to the competition, clicks add, the team selected will then be removed from the main teams list, and added to the competition.teams list. To remove a team, the user selects the team from the options box, and clicks remove. This will remove the team from the competition.teams array, and re-added to the teams array.

  1. I have a list of "teams" in a drop down box, with a button to "add team". When clicked, it will add the team selected from the drop-down box to the select options box. When I remove the Team from the box, it will fail to remove it from the parent list that is data bound to the options box.
  2. When the team is then readded, both entries in the select are the same team name.

The desired outcome

I want the code to work as described above, its possible I have over-engineered the solution due to my limited knowledge of knockout/javascript. I am open to other solutions, I havent got to the stage of submitting this back to the server yet, i predict this will not be as easy as a normal form submit!

The exception

The error in the chrome console is:

Uncaught TypeError: Cannot read property 'name' of undefined at eval (eval at parseBindingsString (knockout-min.3.4.2.js:68), :3:151) at f (knockout-min.3.4.2.js:94) at knockout-min.3.4.2.js:96 at aBi (knockout-min.3.4.2.js:118) at Function.Uc (knockout-min.3.4.2.js:52) at Function.Vc (knockout-min.3.4.2.js:51) at Function.U (knockout-min.3.4.2.js:51) at Function.ec (knockout-min.3.4.2.js:50) at Function.notifySubscribers (knockout-min.3.4.2.js:37) at Function.ha (knockout-min.3.4.2.js:41)

The Code

Teams Multi select和DropDown

The HTML for the screenshot:

<div class="form-group">
    <div class="col-md-3">
        <label for="selectedTeams" class="col-md-12">Select your Teams</label>
        <button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
                class="btn btn-default col-md-12">Add Team</button>
        <button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
                class="btn btn-default col-md-12">Remove Team</button>
        <a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
    </div>

    <div class="col-md-9">
        <select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
                class="dropdown form-control"></select>
        <select id="selectedTeams" name="Teams" class="form-control" size="5"
                data-bind="options:competition().teams(),optionsText:function(item){return item().name;},value:teamToRemove">
                </select>
    </div>
</div>

The addTeam button click code:

self.addTeam = function () {
    if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){// Prevent blanks and duplicates
        self.competition().teams().push(self.teamToAdd);
        self.competition().teams.valueHasMutated();
    }
    self.teams.remove(self.teamToAdd());
    self.teamToAdd(null);
};

the removeTeam button click code:

self.removeTeam = function () {
    self.teams.push(self.teamToRemove());
    self.competition().teams.remove(self.teamToRemove());
    self.competition().teams.valueHasMutated();
    self.teamToRemove(null);
};

the Competition object (some properties removed for brevity):

function Competition(data) {
    var self = this;
    self.id = ko.observable(data.id);
    self.name = ko.observable(data.name);
    self.teams = ko.observableArray(
        ko.utils.arrayMap(data.teams, function (team) {
            return ko.observable(new Team(team));
        }));
};

the team object:

function Team(data) {
    var self = this;
    self.id = ko.observable(data.id);
    self.name = ko.observable(data.name);
}

Anything missing or unclear? Please ask and I will add to the materials on the question.

The Solution

As suggested by @user3297291

The problem was that the objects being added to competition.teams were observable in some places and not observable in others. This was causing a binding error in some places where it would try to access the observable property inside the observable object.

Changed Competition Object

function Competition(data) {
  var self = this;
  self.id = ko.observable(data.id);
  self.name = ko.observable(data.name);
  self.teams = ko.observableArray(
    ko.utils.arrayMap(data.teams, function (team) {
      return new Team(team);
    }));
};

Revised HTML binding (only simplified the optionsText binding)

<div class="form-group">
    <div class="col-md-3">
        <label for="selectedTeams" class="col-md-12">Select your Teams</label>
        <button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
                class="btn btn-default col-md-12">Add Team</button>
        <button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
                class="btn btn-default col-md-12">Remove Team</button>
        <a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
    </div>

    <div class="col-md-9">
        <select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
                class="dropdown form-control"></select>
        <select id="selectedTeams" name="Teams" class="form-control" size="5"
                data-bind="options:competition().teams(),optionsText:'name',value:teamToRemove">
                </select>
    </div>
</div>

Revised Add Team function

self.addTeam = function () {
    if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){
        self.competition().teams().push(self.teamToAdd());
        self.competition().teams.valueHasMutated();
    }
    self.teams.remove(self.teamToAdd());
    self.teamToAdd(null);
};

Revised Remove Team Function

pretty sure I don't need the valueHasMutated() call anymore but at least it works..

self.removeTeam = function () {
    self.teams.push(self.teamToRemove());
    self.competition().teams.remove(self.teamToRemove());
    self.competition().teams.valueHasMutated();
    self.teamToRemove(null);
};

You're filling an observableArray with observable instances. This is something you generally should not do:

// Don't do this:
self.teams = ko.observableArray(
  ko.utils.arrayMap(data.teams, function(team) {
    return ko.observable(new Team(team));
  })
);

Instead, include the Team instances without wrapping them:

// Do this instead:
self.teams = ko.observableArray(
  ko.utils.arrayMap(data.teams, function(team) {
    return new Team(team);
  })
);

Now, you can use the "simple" optionsText binding, like you did earlier:

data-bind="optionsText: 'name', /* ... */" 

Personal preference: you don't need the utils.arrayMap helper when we have .map in every browser. I'd personally write:

Team.fromData = data => new Team(data);
// ...
self.teams = ko.observableArray(data.teams.map(Team.fromData));

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM