knockout contentditable binding with array values - javascript

I am trying to create a custom binding to edit content with HTML5 on a table following this link example and I can't get it to work with an observableArray().
The table is being show in the view with foreach data-bindig like this:
<tbody data-bind="foreach: customers">
<tr data-bind="attr: {id: $index}">
<td style="text-align: center;">
<span class="label label-primary" data-bind="html: Id"></span>
</td>
<td data-bind="html: Name, attr: {id: 'Nome'}, contentEditable: true"></td>
</tr>
</tbody>
The view model is this:
function ViewModel() {
var self = this;
self.data = '#jsonList';
self.customers = ko.observableArray(JSON.parse(self.data));
self.editable = ko.observable(false);
for (i = 0; i < self.customers().length; i++) {
self.customers()[i]['Details'] = '/Anagrafica/Details/' + self.customers()[i]['Id'];
self.customers()[i]['Delete'] = '/Anagrafica/Delete/' + self.customers()[i]['Id'];
};
ko.bindingHandlers.htmlEdit= {
update: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
}
};
ko.bindingHandlers.contentEditable = {
init: function (element, valueAccessor, allBindingsAccessor) {
var value = ko.unwrap(valueAccessor()),
htmlEdit= allBindingsAccessor().htmlEdit;
$(element).on("input", function () {
if (ko.isWriteableObservable(htmlEdit)) {
htmlEdit(this.innerHTML);
}
});
},
update: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
element.contentEditable = value;
$(element).trigger("input");
}
};
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
At the moment I am confused on the code itself, because I don't understand how to set it to point to the elements on the array.
Note: The array is populated, I have no problems in show the content.
Edit: here I add the JSFiddle https://jsfiddle.net/wxn34p45/ for a better read of the code

Looks like you had a spelling mistake Name instead of name:
<td data-bind="html: name, attr: {id: 'Nome'}, contentEditable: true"></td>
Also customers was not an observableArray (in your fiddle at least):
self.customers = ko.observableArray([{"Id":1111,"name":" [Malena]"},{"Id":2222,"name":" [Maria]"},{"Id":3333,"name":" [Merio]"},{"Id":4444,"name":" [Milena]"}]);
I have updated the fiddle (also got it running with knockout, and I have displayed a copy of the ViewModel to help with debugging), the content is now editable.
<h4>View Model</h4>
<pre data-bind="text: ko.toJSON($data, null, 2)"></pre>
Is this now giving the desired effect?
https://jsfiddle.net/asindlemouat/wxn34p45/1/
EDIT
With the further information provided I have managed to make this editable, I have removed the editable from the customers array and used the one in the ViewModel.
As it is looping through the customers array you need to call the editable observable in the ViewModel using $parent.editable.
<tbody data-bind="foreach: customers">
<tr data-bind="attr: {id: $index}">
<td style="text-align: center;">
<span class="label label-primary" data-bind="html: Id"></span>
</td>
<td data-bind="html: name, attr: {id: 'Nome'}, contentEditable: $parent.editable"></td>
<td></td>
</tr>
</tbody>
Updated fiddle: https://jsfiddle.net/asindlemouat/wxn34p45/8/

Related

Set href using $root value within foreach loop

The link in the first td column spazzes out when trying to bind to $root.rootBaseUrl.
In the second td column, the same rootBaseUrl observable prints perfectly.
The difference is that in the first td column, I am trying to set the value within attr:.
Also, please note that there is a foreach loop happening at the tbody level. Hence the use of $root prefix.
<tbody data-bind="foreach: siteList">
<tr>
<td><h3><a data-bind="text: SiteName, attr: {href: $root.rootBaseUrl + SiteID}"></a></h3></td>
<td><h3><span data-bind="text: $root.rootBaseUrl"></span></h3></td>
</tr>
</tbody>
var rootBaseUrl = ko.observable("");
var index = window.location.toString().indexOf("RiskOrder");
var baseURL = window.location.toString().substring(0, index);
this.rootBaseUrl(baseURL);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
Basically, I am getting the current browser URL in the JS, stripping it to the base root url, then trying to add this static URL to the href binding and concatenating with a dynamic SiteID value.
Is this possible?
Replace attr with text and get a glimplse of your problem:
function Vm(){
var self = this;
self.SiteID = ko.observable("AX123");
}
function RootVm(){
var self = this;
var index = window.location.toString().indexOf("RiskOrder");
var baseURL = window.location.toString().substring(0, index);
self.rootBaseUrl = ko.observable("");
self.SiteName = ko.observable("My Site");
self.rootBaseUrl(baseURL);
self.SiteList = ko.observableArray([new Vm()]);
}
ko.applyBindings(new RootVm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table>
<tbody data-bind="foreach: SiteList">
<tr>
<td>
<h3><a data-bind="text: $root.rootBaseUrl + SiteID"></a></h3>
</td>
<td>
<h3><span data-bind="text: $root.rootBaseUrl"></span></h3>
</td>
</tr>
</tbody>
</table>
It renders function....., so a textual representation of a function. This is because rootBaseUrl is a function. If you want to use it in an expression, you have to use parentheses:
<h3><a data-bind="text: $root.rootBaseUrl()+ SiteID()"></a></h3>
If you use it as the only thing in a binding, the parentheses are optional:
<h3><span data-bind="text: $root.rootBaseUrl()"></span></h3>
So your fix would be:
function Vm(){
var self = this;
self.SiteID = ko.observable("AX123");
self.SiteName = ko.observable("My Site");
}
function RootVm(){
var self = this;
var index = window.location.toString().indexOf("RiskOrder");
var baseURL = window.location.toString().substring(0, index);
// On SO Snippets window.location works differently so we hack it:
baseURL = "https://example.com/my-website/url/";
self.rootBaseUrl = ko.observable("");
self.rootBaseUrl(baseURL);
self.SiteList = ko.observableArray([new Vm()]);
}
ko.applyBindings(new RootVm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table>
<tbody data-bind="foreach: SiteList">
<tr>
<td>
<h3><a data-bind="text: SiteName, attr: { href: $root.rootBaseUrl() + SiteID() }"></a></h3>
</td>
<td>
Second column
</td>
</tr>
</tbody>
</table>
Or use a computed so you can (a) unit test the logic and (b) make the parentheses optional again:
function Vm(urlBaseVm){
var self = this;
self.SiteID = ko.observable("AX123");
self.SiteName = ko.observable("My Site");
self.hrf = ko.computed(function() {
return urlBaseVm.rootBaseUrl() + self.SiteID();
});
}
function RootVm(){
var self = this;
var index = window.location.toString().indexOf("RiskOrder");
var baseURL = window.location.toString().substring(0, index);
// On SO Snippets window.location works differently so we hack it:
baseURL = "https://example.com/my-website/url/";
self.rootBaseUrl = ko.observable("");
self.rootBaseUrl(baseURL);
self.SiteList = ko.observableArray([new Vm(self)]);
}
ko.applyBindings(new RootVm());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<table>
<tbody data-bind="foreach: SiteList">
<tr>
<td>
<h3><a data-bind="text: SiteName, attr: { href: hrf }"></a></h3>
</td>
<td>
Second column
</td>
</tr>
</tbody>
</table>

Knockout does not update UI when observableArray is a property of other model

I have 2 observableArray connected to each other. When a "feature" is clicked, I try to show "tasks" of it. However KO, does not update UI when I clicked a feature. On the console, I can track my viewModel, and I can see tasks successfully loaded on selectedFeature. However, UI does not update, even all arrays are defined as observable.
Here is a live demo on fiddle.
Please tell me where I am missing?
function GetFeatures() {
var url = "/Project/GetFeatures";
$.get(url, "", function (data) {
$.each(JSON.parse(data), function (i, item) {
projectVM.features.push(new featureViewModelCreator(item, projectVM.selectedFeature));
});
});
};
function GetTasks(selectedFeature) {
var url = "/Task/GetTaskList";
$.get(url, { "FeatureId": selectedFeature.FeatureId }, function (data) {
$.each(JSON.parse(data), function (i, item) {
selectedFeature.tasks.push(new taskViewModelCreator(item, selectedFeature.selectedTask));
});
});
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
this.IsSelected = ko.computed(function () {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.Status = data.Status.Name;
self.CreatedDate = data.CreatedDate;
self.UserCreatedFullName = data.UserCreated.FullName;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function () {
return selected() === self;
});
self.selectedTask = ko.observable();
self.taskClicked = function (clickedTask) {
var selection = ko.utils.arrayFilter(self.model.tasks(), function (item) {
return clickedTask === item;
})[0];
self.selectedTask(selection);
}
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function (clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(clickedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
On the UI
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<table class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature.tasks">
<tr>
<td><span data-bind="text:Title"></span></td>
</tr>
</tbody>
</table>
</div>
Here is the correct version with key notes: here. KO documentation is quite a detailed one.
You have mentioned an interesting note about UI code style: "As I know, we don't use () on UI". I did not put attention to this fact before.
We can really omit brackets for an observable: ko observable;
View contains an observable with no brackets:
<label>
<input type="checkbox" data-bind="checked: displayMessage" /> Display message
</label>
Source code:
ko.applyBindings({
displayMessage: ko.observable(false)
});
We can omit brackets for an observable array on UI: ko observable array
View contains: <ul data-bind="foreach: people">, while
View model has:
self.people = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
We can omit brackets on UI for 'leaf' observables or observables arrays. Here is your modified code sample. data-bind="if: selectedFeature" and data-bind="foreach: selectedFeature().tasks"> only leaf observable braces are omitted.
Finally, can we omit brackets for 'parent' observables? We can do it by adding another ko UI-statement (with instead of if, example 2).
The with binding will dynamically add or remove descendant elements
depending on whether the associated value is null/undefined or not
But, I believe, we can not omit brackets for parent nodes outside UI statement, because it is equal to a javascript statement: projectVM.selectedfeature().tasks. Othervise projectVM.selectedfeature.tasks will not work, because observables does not have such property tasks. Instead an observable contains an object with that property, which is retrieved by calling it via brackets (). There is, actually, an example on knockoutjs introduction page. <button data-bind="enable: myItems().length < 5">Add</button>
The code below uses the following fact (which can be found here, example 2):
It’s important to understand that the if binding really is vital to
make this code work properly. Without it, there would be an error when
trying to evaluate capital.cityName in the context of “Mercury” where
capital is null. In JavaScript, you’re not allowed to evaluate
subproperties of null or undefined values.
function GetFeatures() {
var data = {
Name: "Test Feature",
FeatureId: 1
}
projectVM.features.push(new featureViewModelCreator(data, projectVM.selectedFeature));
};
function GetTasks(selectedFeature) {
var data = {
Title: "Test Feature",
TaskId: 1
}
selectedFeature().tasks.push(new taskViewModelCreator(data, selectedFeature().selectedTask));
};
function taskViewModelCreator(data, selected) {
var self = this;
self.TaskId = data.TaskId;
self.Title = data.Title;
// Step 3: you can omit $root declaration, I have removed it
// just to show that the example will work without $root as well.
// But you can define the root prefix explicitly (declaring explicit
// scope may help you when you models become more complicated).
// Step 4: data-bind="if: selectedFeature() statement was added
// to hide the table when it is not defined, this statement also
// helps us to avoid 'undefined' error.
// Step 5: if the object is defined, we should referense
// the observable array via -> () as well. This is the KnockoutJS
// style we have to make several bugs of that kind in order
// to use such syntax automatically.
this.IsSelected = ko.computed(function() {
return selected() === self;
});
}
function featureViewModelCreator(data, selected) {
var self = this;
self.FeatureId = data.FeatureId;
self.Name = data.Name;
self.tasks = ko.observableArray();
this.IsSelected = ko.computed(function() {
return selected() === self;
});
self.selectedTask = ko.observable();
}
function projectViewModelCreator() {
var self = this;
self.ProjectId = 1;
self.features = ko.observableArray();
self.selectedFeature = ko.observable();
self.featureClicked = function(clickedFeature) {
self.selectedFeature(clickedFeature);
GetTasks(self.selectedFeature);
}
}
var projectVM = new projectViewModelCreator();
ko.applyBindings(projectVM, $('.taskmanTable')[0]);
GetFeatures();
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="taskmanTable">
<table class="table table-hover featureList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: features">
<tr data-bind="click: $root.featureClicked, css: { active : IsSelected } ">
<td><span data-bind="text: Name"> </span></td>
</tr>
</tbody>
</table>
<hr/>
<table data-bind="if: selectedFeature()" class="table table-hover taskList">
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody data-bind="foreach: selectedFeature().tasks()"><!-- $root -->
<tr>
<td><span data-bind="text: Title"></span></td>
</tr>
</tbody>
</table>
</div>

Knockout foreach nesting issue

I'm trying to generate a table using properties of two viewmodels, which are sub viewmodels of the main viewmodel, which ko.applyBindings() is called using.
The idea is to generate a row for each element in SubVM1 where the first cell is the element's name. Then for every element in SubVM2, an additional cell is added to the row.
The rows generate correctly and the first cell shows the SubVM1 name, but it is only followed by one cell, instead of however many elements are in SubVM2.
Also, the function in the data-bind doesn't work either. I've tried declaring the Value function as a prototype of SubV2 but it errors out as undefined.
Regardless, something I'm not sure about is clearly happening with the binding context, and help would be appreciated.
<tbody data-bind="foreach: {data: SubViewModel1, as: 'SubVM1'}">
<tr>
<td data-bind="text: SubVM1.Name" />
<!-- ko foreach: {data: $root.SubViewModel2, as: 'SubVM2'} -->
<td data-bind="text: Value(SubVM1.Type)"></td>
<!-- /ko -->
</tr>
</tbody>
Edit: Partially done jsfiddle: http://jsfiddle.net/jgr71o8t/1
There are a couple of things that I could see.
The first one is that the <td data-bind='' />.
Knockout does not generally like self closed tags. Always use the closing tag, in this case <td data-bind=''></td>
The second is that anything you want updated on the screen should be an ko.observable or ko.observableArray. any changes to properties after ko.applyBindings will not be reflected on the screen
HTML
<table border="1">
<tbody data-bind="foreach: {data: subViewModel1, as: 'SubVM1'}">
<tr>
<td data-bind="text: name"></td>
<!-- ko foreach: {data: $root.subViewModel2, as: 'SubVM2'} -->
<td data-bind="text: SubVM2.Value(SubVM1.Type)"></td>
<!-- /ko -->
</tr>
</tbody>
</table>
JS Fiddle Demo with knockout bindings on all properties
function MasterViewModel() {
var self = this;
self.subViewModel1 = ko.observableArray([]);
self.subViewModel2 = ko.observableArray([]);
}
function SubVM1ViewModel() {
var self = this;
self.name = ko.observable("Sub View Model 1");
self.otherProperty = ko.observable(43);
}
function SubVM2ViewModel() {
var self = this;
self.title = ko.observable('Sub View Model 2');
self.subVM1List = ko.observableArray([]);
}
SubVM2ViewModel.prototype.Value = function (type) {
for (var i = 0; i < this.subVM1List().length; i++) {
if (this.subVM1List()[i].Type === type) {
return this.subVM1List()[i].name();
}
}
};
var masterVM = new MasterViewModel();
var subVM2 = new SubVM2ViewModel();
subVM2.subVM1List.push(new SubVM1ViewModel());
masterVM.subViewModel1.push(new SubVM1ViewModel());
masterVM.subViewModel2.push(subVM2);
ko.applyBindings(masterVM);
JS Fiddle Demo with straight javascript properties
function MasterViewModel() {
var self = this;
self.subViewModel1 = [];
self.subViewModel2 = [];
}
function SubVM1ViewModel() {
var self = this;
self.name = "Sub View Model 1";
self.otherProperty =43;
}
function SubVM2ViewModel() {
var self = this;
self.title = 'Sub View Model 2';
self.subVM1List = [];
}
SubVM2ViewModel.prototype.Value = function (type) {
for (var i = 0; i < this.subVM1List.length; i++) {
if (this.subVM1List[i].Type === type) {
return this.subVM1List[i].name;
}
}
};
var masterVM = new MasterViewModel();
var subVM2 = new SubVM2ViewModel();
subVM2.subVM1List.push(new SubVM1ViewModel());
masterVM.subViewModel1.push(new SubVM1ViewModel());
masterVM.subViewModel2.push(subVM2);
ko.applyBindings(masterVM);

Trouble binding tables to Knockout collection

I can't understand why these Knockout table bindings aren't working:
Javascript:
$(function () {
var FileObject = function(id, name) {
this.id = id;
this.name = name;
};
var FilesModel = function() {
this.filesSelected = ko.observable(false);
this.myFiles = ko.observableArray([new FileObject(1, 'test_1')]);
this.myFiles.push(new FileObject(2, 'test_2'));
};
var filesModel = new FilesModel();
window.filesModel = filesModel;
ko.applyBindings(filesModel);
filesModel.myFiles().push(new FileObject(3, 'test_3')); // This never shows
alert(filesModel.myFiles().length); // Shows 3 items
});
HTML:
<h3>TABLE 1</h3>
<table>
<tbody data-bind="foreach: myFiles">
<tr>
<td>FILE:</td>
<td data-bind="text: name"></td>
</tr>
</tbody>
</table>
<h3>TABLE 2</h3>
<table>
<tbody data-bind="foreach: myFiles()">
<tr>
<td>FILE:</td>
<td data-bind="text: name"></td>
</tr>
</tbody>
</table>
In both of these tables, the first 2 files will show, but the 3rd file doesn't. What am I missing?
You're really close. Two main things to point out:
Status isn't an observable, and you're attempting to unwrap it with text: status().
You're pushing the new FileObject into an unwrapped array, meaning you're bypassing the observable altogether. Push new items directly into the observable array, you'll have better luck.
I've put together a jsbin example based on your original source.
Specifically, this:
filesModel.myFiles().push(new FileObject(3, 'test_3')); // This never shows
Should be:
filesModel.myFiles.push(new FileObject(3, 'test_3')); // Now it does
In your HTML, you were trying to data-bind status(), but status is not an observable. One approach is to make your FileObject members observables.
Also, your third FileObject was never showing because your syntax was wrong. Instead of filesModel.myFiles().push, it should be just filesModel.myFiles.push
See updated fiddle
$(function () {
var FileObject = function(id, name, size, status, progress) {
this.id = ko.observable(id);
this.name = ko.observable(name);
this.status = ko.observable(status);
};
var FilesModel = function() {
this.filesSelected = ko.observable(false);
this.myFiles = ko.observableArray([new FileObject(1, 'test_1')]);
this.myFiles.push(new FileObject(2, 'test_2', 3, 'status'));
};
var filesModel = new FilesModel();
window.filesModel = filesModel;
ko.applyBindings(filesModel);
filesModel.myFiles.push(new FileObject(3, 'test_3')); // This never shows
alert(filesModel.myFiles().length); // Shows 3 items
});

No value is coming in my partial view throught knockout js

var baseUri = '#ViewBag.ApiUrl';
var viewmodel = function () {
var self = this;
self.VoucherDetails = ko.observableArray([]);
$.getJSON(baseUri, this.VoucherDetails);
alert(self.VoucherDetails);
};
<table>
<tbody data-bind='foreach: VoucherDetails'>
<tr>
<td data-bind="text : $data.empcode">
<span data-bind=" text : $data.empcode"></span> test
</td>
<td data-bind=" text : empcode">
<span data-bind=" text : empcode"></span>
</td>
</tr>
</tbody>
</table>
This is my partial view and there is no value coming in empcode or $data.empcode, I am new to knockoutjs. What is wrong in my code?
Your code looks incomplete:
$.getJson never sets the value of VoucherDetails (no callback is passed).
You never call ko.applyBindings
It should be:
var baseUri = '#ViewBag.ApiUrl';
var viewmodel = function () {
var self = this;
self.VoucherDetails = ko.observableArray([]);
$.getJSON(baseUri, function(data){
//create your VoucherDetails here based on the data (push each item to VoucherDetails using $.each
});
};
ko.applyBindings(viewModel);
http://knockoutjs.com/documentation/observableArrays.html

Categories