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.
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 a.B.i (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

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.
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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With