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 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
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));
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.