For some reason, foreach in Knockout.js doesn't iterate through my observable array.
In my HTML I have this which works perfectly fine with the observable model:
<div class="field-group">
<label class="popup-label" for="email">Email</label>
<span class="email" data-bind="text: masterVM.employeeVM.Email"></span>
</div>
But in the same model, this code doesn't work:
<ul data-bind="foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }">
<li>
<span class="popup-tag" data-bind="text: tag.tagName"><i class="zmdi zmdi-delete"></i></span>
</li>
</ul>
There are two models:
Employee
var observableEmployee = function(id, email, tags) {
var self = this;
self.Id = ko.observable(id);
self.Email = ko.observable(email);
self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
}));
self.errors = ko.validation.group(this, {
deep: true
});
self.isValid = ko.computed(function() {
return self.errors().length > 0 ? false : true;
});
}
and Tag
var observableTag = function(id, employeeId, tagId, tagName) {
var self = this;
self.Id = ko.observable(id);
self.employeeId = ko.observable(employeeId);
self.tagId = ko.observable(tagId);
self.TagName = ko.observable(tagName);
self.errors = ko.validation.group(this, {
live: true
});
self.isValid = ko.computed(function() {
return self.errors().length > 0 ? false : true;
});
}
and handler function:
var employeeHandler = function () {
var self = this;
self.getEmployeeDetails = function (header) {
$.ajax({
url: masterVM.controller.renderEmployeeDetails,
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify({ id: header.data("employeeid") }),
success: function (result) {
masterVM.employeeVM = new observableEmployee(
result.model.Id,
result.model.Email,
result.model.Tags
);
ko.applyBindings(masterVM, $("#employee-planning-selected")[0]);
//header.parent().addClass('open');
//header.next().slideDown('normal');
//hideLoader(header);
console.log('get employee details');
$(document).on('click', "div.employee", onNameCardClick);
},
error: function (xhr, ajaxOptions, thrownError) {
alert('Error!');
}
});
}}
In my HTML file
<script>
masterVM = {
controller: {
renderEmployeeDetails: '#(Html.GetActionUrl<EmployeesController>(c => c.RenderEmployeeDetails(0)))'
},
employeeHandler: new employeeHandler(),
employeeVM: new observableEmployee(0, '', '', '', '')
}
ko.applyBindings(masterVM);
</script>
Tried something like this, and still nothing
<!--ko foreach: employeeVM.Tags -->
<span data-bind="text: $data.Tags"></span>
<!-- /ko -->
And no, there are no errors in the console, I have used KnockouJS context debugger which shows me that there are elements in this collection, even when I try to display them as an object it shows me a list of 4 elements.
Knockout version: 2.3.0
1). If you are binding masterVM object in ko.applyBindings(masterVM), you don't need to specify that object again in your data-bindings.
So, it should be
foreach: { data: employeeVM.Tags, as: 'tag' }
And not
foreach: { data: masterVM.employeeVM.Tags, as: 'tag' }
(I'm not sure how the first data-bind="text: masterVM.employeeVM.Email" is working)
2). You don't need to call applyBindings more than once. If you want to update the employee object, you can turn your employeeVM into an observable and keep updating it inside getEmployeeDetails method.
3) Your containerless control flow syntax won't work. (<!--ko foreach: employeeVM.Tags -->). Inside this foreach, $data is the current Tag object in context. So, it should be <span data-bind="text: $data.TagName"></span>
Here's a minimal version of the code. Click on "Run code snippet" to test it. When you click on Update employee button, I'm updating the employeeVM observable and the data gets rendered again. Without calling applyBindings again
var employeeHandler = function() {
var self = this;
self.getEmployeeDetails = function(header) {
var newEmployee = new observableEmployee(0, 'newEmployee#xyz.com', [{
Id: 3,
EmployeeId: 3,
TagId: 3,
tagName: 'Tag Name 3'
}]);
// You need to use employeeVM(newEmployee) instead of employeeVM = newEmployee
// Because employeeVM is an observable.
masterVM.employeeVM(newEmployee);
}
}
var observableEmployee = function(id, email, tags) {
var self = this;
self.Id = ko.observable(id);
self.Email = ko.observable(email);
self.Tags = ko.observableArray(ko.utils.arrayMap(tags, function(item) {
return new observableTag(item.Id, item.EmployeeId, item.TagId, item.tagName)
}));
}
var observableTag = function(id, employeeId, tagId, tagName) {
var self = this;
self.Id = ko.observable(id);
self.employeeId = ko.observable(employeeId);
self.tagId = ko.observable(tagId);
self.TagName = ko.observable(tagName);
}
var masterVM = {
controller: {
renderEmployeeDetails: ''
},
employeeHandler: new employeeHandler(),
// change this to an observable
employeeVM: ko.observable(new observableEmployee(0, 'abc#xyz.com', [{
Id: 1,
EmployeeId: 1,
TagId: 1,
tagName: 'Tag name 1'
}]))
}
ko.applyBindings(masterVM);
document.getElementById("button").addEventListener("click", function(e) {
masterVM.employeeHandler.getEmployeeDetails()
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<div class="field-group">
<label class="popup-label" for="email">Email:</label>
<span class="email" data-bind="text: employeeVM().Email"></span>
</div>
<ul data-bind="foreach: { data: employeeVM().Tags, as: 'tag' }">
<li>
<span class="popup-tag" data-bind="text: tag.employeeId"></span> <br>
<span class="popup-tag" data-bind="text: tag.tagId"></span><br>
<span class="popup-tag" data-bind="text: tag.TagName"></span>
</li>
</ul>
<button id="button">Update employee</button>
Related
Using Knockout and Semantic UI.
I'm trying to figure out how to get the values selected for my multi select dropdown. The first dropdown works with just single values, but the multi select one dosent. I have an observable array inside another collection:
<tbody id="tbodyelement" data-bind="foreach: groupUserCollection">
<tr>
<td>
<div class="ui selection dropdown fluid">
<input type="hidden" name="groupDD" data-bind="value: group.name">
<i class="dropdown icon"></i>
<div class="default text">Select Group</div>
<div class="menu" data-bind="foreach: $parent.groupCollection">
<div class="item" data-bind="text: $data.name(), attr: {'data-value': $data.id()}"></div>
</div>
</div>
</td>
<td>
<div class="ui multiple selection dropdown long-width" id="multi-select">
<div data-bind="foreach: user">
<input type="hidden" name="userDD" data-bind="value: firstLastName">
</div>
<div class="default text">Select User</div>
<div class="menu" data-bind="foreach: $parent.userCollection">
<div class="item" data-bind="text: $data.firstLastName(), attr: {'data-value': $data.id()}"></div>
</div>
<i class="dropdown icon"></i>
</div>
</td>
</tr>
</tbody>
I have one model groupuser that has a group model in it and a collection of roles.
var groupUser = function (data) {
var self = this;
self.group = ko.mapping.fromJS(data.group),
self.user = ko.observableArray([]),
self.id = ko.observable(data.id),
self.group.subscribe = function () {
showButtons();
},
self.user.subscribe = function () {
// self.user.push(data.user);
showButtons();
}
};
var group = function (data) {
var self = this;
self.id = ko.observable(data.id),
self.name = ko.observable(data.name),
self.project = ko.observable(data.project),
self.projectId = ko.observable(data.projectId),
self.role = ko.observable(data.role),
self.roleId = ko.observable(data.roleId)
};
var user = function (data) {
var self = this;
self.id = ko.observable(data.id),
self.accountId = ko.observable(data.accountId),
self.email = ko.observable(data.email),
self.firstName = ko.observable(data.firstName),
self.lastName = ko.observable(data.lastName),
self.firstLastName = ko.pureComputed({
read: function()
{
return self.firstName() + " " + self.lastName();
}
,
write: function(value)
{
var lastSpacePos = value.lastIndexOf(" ");
if (lastSpacePos > 0) {
self.firstName(value.substring(0, lastSpacePos));
self.lastName(value.substring(lastSpacePos + 1));
}
console.log("firstname: " + self.firstName());
}
}),
};
groupViewModel = {
groupUserCollection: ko.observableArray(),
userCollection: ko.observableArray(),
groupCollection: ko.observableArray()
}
I add the data using this function:
$(data).each(function (index, element) {
var newGroup = new group({
id: element.group.id,
name: element.group.name,
project: element.group.project,
projectId: element.group.projectId,
role: element.group.role,
roleId: element.group.roleId
});
newGroup.id.subscribe(
function () {
newGroupUser.showButtons();
}
);
newGroup.name.subscribe(
function () {
newGroupUser.showButtons();
}
);
var newGroupUser = new groupUser({
group: newGroup,
id: element.id,
});
ko.utils.arrayForEach(element.user, function (data) {
var newUser = new user({
id: data.id,
accountId: data.accountId,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
});
newUser.id.subscribe(
function () {
newGroupUser.showButtons();
}
);
newUser.firstName.subscribe(
function () {
newGroupUser.showButtons();
}
);
newUser.lastName.subscribe(
function () {
newGroupUser.showButtons();
}
);
newGroupUser.user.push(newUser);
});
groupViewModel.groupUserCollection.push(newGroupUser);
});
I ended up adding in a custom bind to the data-bind on the hidden input and it worked. But now my subscription dosent work when I add values or remove them.
Code that worked:
ko.bindingHandlers.customMultiBind = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
ko.utils.arrayForEach(bindingContext.$data.user(), function (data) {
if (element.value === "")
{
element.value = ko.utils.unwrapObservable(data.id)
}
else {
element.value = element.value + "," + ko.utils.unwrapObservable(data.id)
}
});
}
};
I have a drop down that's dynamically created, when it first get's created it has 9 objects with different properties. When you click on an option it goes out and grabs more info and in this case gets 3 new objects. But when it updates it goes from this:
Initial drop down Selection
to This:
Updated List after click
The objects are updated and when console logged they both look good. However, it appears to be updating by how many items are in the array
view:
#if (Model.SubMenu.Count > 8)
{
<li style="display:inline;" id="MenuSearch">
<input id="MenuSearchInput" class="form-control" type="text" style="margin:9px; width:90%" placeholder="Search" data-bind="value: query, valueUpdate: 'keyup'">
</li>
<li class="divider"></li>
<li id="MenuSearchItem" role="menuitem" data-bind="foreach: FilteredItems">
<a data-bind="text: name, click: Inventory.GetDomainFacilities"></a>
</li>
}
ViewModel:
(function (i) {
"use strict";
var vm = i.ViewModels || (i.ViewModels = {});
vm.DomainSwitchViewModel = function (menuItem) {
var self = this;
var data = menuItem;
self.query = ko.observable('');
self.AllMenuItems = ko.observableArray([]);
self.Update = function (data) {
var mappedItems = _.map(data, function (item) {
if (item.FacilityID) {
return {
id: item.FacilityID,
name: item.FacilityName,
type: 'Facility',
href: '/Home/SwitchDomain?' + 'domainID=' + item.DomainID + 'facilityID=' + item.FacilityID
}
}
else {
return {
id: item.DomainID,
name: item.Name,
type: 'Domain',
href: ''
}
}
})
self.AllMenuItems(mappedItems);
};
self.FilteredItems = ko.computed(function () {
var allItems = self.AllMenuItems();
var search = self.query().toLowerCase();
return ko.utils.arrayFilter(allItems, function (item) {
return item.name.toLowerCase().indexOf(search) >= 0;
});
});
self.Update(menuItem);
}
window.Inventory = i;
})(window.Inventory || {});
GetDomainFacilities:
i.GetDomainFacilities = function (menuItem) {
$.ajax({
type: 'GET',
url: '/InventoryBase/GetDomainFacilities',
data: { domainID: menuItem.id },
contentType: 'application/json',
dataType: 'json',
success: function (data) {
var updatedMenuItem = JSON.parse(data);
vm.Update(updatedMenuItem);
ko.cleanNode(document.getElementById("DropDownNavMenus"))
ko.applyBindings(vm, document.getElementById("DropDownNavMenus"));
}
});
}
I have a JS object:
var bookmark = {
id: 'id',
description: 'description',
notes: 'notes'
}
I want to bind to the entire object, display notes in a textarea, and subscribe to changes to notes.
Here's what I have so far:
this.bookmark = ko.observable();
this.bookmark.subscribe = function(bookmarkWithNewNotes) {
//use the bookmarkWithNewNotes.id to update the bookmark in the db
}
I'm setting the bookmark like so:
this.bookmark(ko.mapping.fromJS(existingBookmark));
The view looks like this:
<div databind="with: $root.bookmark" >
Notes
<textarea class="userNotes" rows="10" data-bind="value: notes" ></textarea>
</div>
This isn't working. What do I need to do to make this work the way I want it to work?
Thanks!
Here is a example in Fiddle.
You could do something like this:
<div>
Notes
<div data-bind="foreach: bookmarks">
<textarea rows="10" data-bind="value: note"></textarea>
</div>
</div>
and create viewmodel for your bookmark, like so:
function BookmarkViewModel(id, description, note) {
var self = this;
self.id = id;
self.description = ko.observable(description);
self.note = ko.observable(note);
self.note.subscribe(function(val) {
alert("Save note, id: " + self.id + ", description: " + self.description() + ", note: " + self.note());
});
return self;
}
after you get your data, create VM for every item, like so:
function AppViewModel(data) {
var self = this;
self.bookmarks = ko.observableArray();
for (var i = 0; i < data.length; i++) {
self.bookmarks().push(new BookmarkViewModel(data[i].id, data[i].description, data[i].note));
};
return self;
}
You can create a seperate service to get your data, i just mocked this for poc.
$(function() {
var data = [{
id: 1,
description: 'some description',
note: 'some note'
}, {
id: 2,
description: 'some other description',
note: 'some other note'
}];
ko.applyBindings(new AppViewModel(data));
});
I have created my first KO component :
components.js
ko.components.register('team-dropdown', {
viewModel: function (params) {
var self = this;
self.teamNames = ko.observableArray([]);
$.ajax({
url: 'http://footballcomps.cloudapp.net/Teams',
type: 'get',
contentType: 'application/json',
success: function (data) {
$.each(data['value'], function (key, value) {
self.teamNames.push(value.TeamName);
});
console.dir(self.teamNames);
},
error: function (err) {
console.log(err);
}
});
self.selectedTeam = ko.observable();
},
template: { require: 'text!components/team-dropdown.html' }
});
team-dropdown.html
<div id="teams" class="inputBlock form-group">
<select class="form-control" name="teamName" data-bind="options: teamNames, value:selectedTeam"></select>
<label id="lblHomeTeam" data-bind="text: selectedTeam"></label>
And here is my view where I want to use the component :
<div class="row" id="divFixture">
<div class="col-md-4">
<div class="panel panel-info">
<div class="panel-heading">
<h2 class="panel-title">Add new fixture</h2>
</div>
<div class="panel-body">
<form data-bind="submit: fixture.addFixture">
<div class="form-group">
<team-dropdown />
</div>....
</form>
And my stripped down view model :
define(['knockout', 'knockout.validation', 'common', 'components'], function (ko) {
return function fixtureViewModel() {
function fixture(fixtureId, fixtureDate, homeId, homeName, homeBadge, homeScore, awayId, awayName, awayBadge, awayScore) {
this.FixtureId = fixtureId;
this.FixtureDate = fixtureDate;
this.HomeId = homeId;
this.HomeName = homeName;
this.HomeBadge = homeBadge;
this.HomeScore = homeScore;
this.AwayId = awayId;
this.AwayName = awayName;
this.AwayBadge = awayBadge;
this.AwayScore = awayScore;
}
var self = this;
self.Id = ko.observable();
self.FixtureDate = ko.observable();
self.HomeId = ko.observable();
self.HomeName = ko.observable();
self.HomeBadge = ko.observable();
self.HomeScore = ko.observable();
self.AwayId = ko.observable();
self.AwayName = ko.observable();
self.AwayBadge = ko.observable();
self.AwayScore = ko.observable();
self.selectedTeam = ko.observable();
self.addFixture = function() {
//how do I reference the selected item from my component here?
};
});
How do I reference the item I have selected in my component in self.addFixture?
Since the team-dropdown is meant to be a reusable component, you should provide a way to bind to it. As you have it, it is a standalone control, the outside world cannot interact with it except through the observables you have defined which doesn't make it very flexible.
I would add parameters to it where you can set what observables to bind to the value. Your fixtures has a selectedTeam property so that seems like a likely candidate.
ko.components.register('team-dropdown', {
viewModel: function (params) {
var self = this,
teamNames = ko.observableArray([]),
// default to a local observable if value not provided
selectedTeam = params.value || ko.observable();
// you probably don't want others to directly modify the teamNames array
self.teamNames = ko.pureComputed(teamNames);
self.selectedTeam = selectedTeam;
$.ajax({
url: 'http://footballcomps.cloudapp.net/Teams',
type: 'get',
contentType: 'application/json',
success: function (data) {
$.each(data['value'], function (key, value) {
// push to the local `teamNames` array
teamNames.push(value.TeamName);
});
console.dir(teamNames);
},
error: function (err) {
console.log(err);
}
});
},
template: { require: 'text!components/team-dropdown.html' }
});
Then set the parameter when you use the component:
<form data-bind="submit: fixture.addFixture">
<div class="form-group">
<team-dropdown params="value: fixture.selectedTeam" />
</div>
</form>
The selected value should now be set in the selectedTeam of your fixture, so you can just use that.
self.addFixture = function() {
var selectedTeam = self.selectedTeam(); // should have the value
};
I am trying to add the Rich Text Editor to my Survey system using CKeditor and knockout. I have my ViewModel, which has an observerable array of quesitons. I want to make the Name in each question use the ckeditor. I have look at the post Knockout.js: array parameter in custom binding. And have immplemented that but my OnBlur is not working. The ValueAccessor() is not returning an observable object. So I get an error that string is not a function() on this line of code..
var observable = valueAccessor();
observable($(element).val());
Here is my Html, I am just using a static Id for now on question, and was going to change that after I got this to work for just one question in the array.
<tbody data-bind="foreach: questionModel">
<tr>
<td>
<button data-bind='click: $root.addQuestion' class="btn btn-success" title="Add Question"><i class="icon-plus-sign fontColorWhite"></i></button>
<button data-bind='click: $root.removeQuestion' class="btn btn-danger" title="Remove Question"><i class="icon-minus-sign fontColorWhite"></i></button>
</td>
<td><textarea id="question123" class="RichText" data-bind="richText: Name"></textarea></td>
<td><input type="checkbox" data-bind="checked: AllowComment" /></td>
<td><button data-bind="click: $root.addAnswer" class="btn btn-success" title="Add Answer"><i class="icon-plus-sign fontColorWhite"></i></button></td>
<td>
<div data-bind="foreach: possibleAnswerModel">
<input style="width: 278px" style="margin-bottom: 5px;" data-bind='value: Name' />
<button data-bind='click: $root.removeAnswer' class="btn btn-danger" title="Remove Answer"><i class="icon-minus-sign fontColorWhite"></i></button>
</div>
</td>
</tr>
<tr>
</tbody>
Below is my ViewModel as well as my custom binding....
ko.bindingHandlers.richText = {
init: function (element, valueAccessor, allBindingsAccessor, ViewModel) {
var txtBoxID = $(element).attr("id");
console.log("TextBoxId: " + txtBoxID);
var options = allBindingsAccessor().richTextOptions || {};
options.toolbar_Full = [
['Bold', 'Italic'],
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent'],
['Link', 'Unlink']
];
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
if (CKEDITOR.instances[txtBoxID]) {
CKEDITOR.remove(CKEDITOR.instances[txtBoxID]);
};
});
$(element).ckeditor(options);
//wire up the blur event to ensure our observable is properly updated
CKEDITOR.instances[txtBoxID].focusManager.blur = function () {
console.log("blur");
console.log("Value: " + valueAccessor());
console.log("Value: " + $(element).val());
var observable = valueAccessor();
observable($(element).val());
};
},
update: function (element, valueAccessor, allBindingsAccessor, ViewModel) {
var value = valueAccessor();
console.log("Value Accessor: " + value);
var valueUnwrapped = ko.utils.unwrapObservable(value);
//var val = ko.utils.unwrapObservable(valueAccessor());
console.log("Value: " + valueUnwrapped);
$(element).val(valueUnwrapped);
}
};
function ViewModel(survey) {
// Data
var self = this;
self.StartDate = ko.observable(survey.StartDate).extend({ required: { message: 'Start Date is required' } });
self.EndDate = ko.observable(survey.EndDate).extend({ required: { message: 'End Date is required' } });
self.Name = ko.observable(survey.Name).extend({ required: { message: 'Name is required' } });
self.ButtonLock = ko.observable(true);
self.questionModel = ko.observableArray(ko.utils.arrayMap(survey.questionModel, function(question) {
return { Id: question.QuestionId, Name: ko.observable(question.Name), Sort: question.Sort, IsActive: question.IsActive, AllowComment: question.AllowComment, possibleAnswerModel: ko.observableArray(question.possibleAnswerModel) };
}));
// Operations
self.addQuestion = function () {
self.questionModel.push({
Id: "0",
Name: "",
AllowComment: true,
Sort: self.questionModel().length + 1,
possibleAnswerModel: ko.observableArray(),
IsActive:true
});
};
self.addAnswer = function (question) {
question.possibleAnswerModel.push({
Id: "0",
Name: "",
Sort: question.possibleAnswerModel().length + 1,
IsActive:true
});
};
self.GetBallotById = function (id) {
for (var c = 0; c < self.BallotProjectStandardList().length; c++) {
if (self.BallotProjectStandardList()[c].BallotId === id) {
return self.BallotProjectStandardList()[c];
}
}
return null;
};
self.removeQuestion = function(question) { self.questionModel.remove(question); };
self.removeAnswer = function(possibleAnswer) { $.each(self.questionModel(), function() { this.possibleAnswerModel.remove(possibleAnswer) }) };
self.save = function() {
if (self.errors().length == 0) {
self.ButtonLock(true);
$.ajax("#Url.Content("~/Survey/Create/")", {
data: ko.toJSON(self),
type: "post",
contentType: 'application/json',
dataType: 'json',
success: function(data) { self.successHandler(data, data.success); },
error: function() {
self.ButtonLock(true);
self.errorHandler();
}
});
} else {
self.errors.showAllMessages();
}
};
}
ViewModel.prototype = new ErrorHandlingViewModel();
var mainViewModel = new ViewModel(#Html.Raw(jsonData));
mainViewModel.errors = ko.validation.group(mainViewModel);
ko.applyBindings(mainViewModel);
I figured what I was doing wrong. When I define the observableArray() I was defining the object as ko.observable, however, when I add a question to the array, I was initializing it as a string. So I change that to match and it worked like a champ. Here is the change push.
self.questionModel = ko.observableArray(ko.utils.arrayMap(survey.questionModel, function(question) {
return { Id: question.QuestionId, Name: ko.observable(question.Name), Sort: question.Sort, IsActive: question.IsActive, AllowComment: question.AllowComment, possibleAnswerModel: ko.observableArray(question.possibleAnswerModel) };
}));
// Operations
self.addQuestion = function () {
self.questionModel.push({
Id: "0",
Name: ko.observable(),
AllowComment: true,
Sort: self.questionModel().length + 1,
possibleAnswerModel: ko.observableArray(),
IsActive:true
});
};