Say I have the following knockout view, how can I obtain the outerHtml for the actual generated code with javascript. Whenever I try to select the outerHtml of "table_1" with javascript I end up with the html containing the knockout markup, rather than the actual HTML visible on screen.
<table id="table_1">
<thead>
<tr>
<th>Name</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<!-- ko foreach: $data.Rows -->
<tr>
<td data-bind="text: Name"></td>
<td data-bind="text: Date"></td>
</tr>
<!-- /ko -->
</tbody>
knockout components can provide the bound element.
see http://knockoutjs.com/documentation/component-registration.html
ko.components.register('printable-component', {
viewModel: {
createViewModel: function(params, componentInfo) {
console.log(componentInfo.element);
}
}
});
using this mechanism a 'printable' component can be developed that has access to the innerHTML of the bound element.
// This is a simple *viewmodel* - JavaScript that defines the data and behavior of your UI
function AppViewModel() {
this.Rows = [{
Name: 'Joseph',
Date: '2017-02-13'
}, {
Name: 'Mary',
Date: '2017-02-13'
}];
}
// define a printable component
ko.components.register('printable-component', {
viewModel: {
// create using the factory function
// see http://knockoutjs.com/documentation/component-registration.html
createViewModel: function(params, componentInfo) {
// return a new ViewModel for the component
return new function() {
this.rows = params.rows;
// track the componentInfo
this.componentInfo = componentInfo;
// print method function
this.print = function() {
alert(componentInfo.element.innerHTML);
}
}
}
},
// the component template
// note: can be jsx
// note: can be defined in html using internal template nodes
// note: print button can be hidden using CSS or by defining it outisde the printable component and use the params to start the print function
template: '<table><thead><tr><th>Name</th><th>Date</th></tr></thead><tbody><!-- ko foreach: $data.rows --> <tr><td data-bind="text: Name"></td><td data-bind="text: Date"></td></tr><!-- /ko --></tbody></table>print'
});
// Activates knockout.js
ko.applyBindings(new AppViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<printable-component params="rows:$data.Rows"></printable-component>
Related
I have a component that renders table and I need to define cell template, that will be rendered in each row.
<my-table params="files: unprocessed">
<cell-template>
<span data-bind="text: name" />
</cell-template>
</my-table>
However, the cell-template renders only in the first row. How do I define and use template as a parameter, that will be rendered used inside binding?
<template id="my-table-template">
<table class="table table-striped" data-bind="visible: files().length > 0">
<tbody data-bind="foreach: files()">
<tr>
<td data-bind="text: id" />
<td>
<!-- ko template: { nodes: $parent.cellTemplateNodes} -->
<!-- /ko -->
</td>
</tr>
</tbody>
</table>
</template>
js:
function getChildNodes(allNodes: Array<any>, tagName: string) {
var lowerTagName = tagName.toLowerCase(),
node = ko.utils.arrayFirst(allNodes, function (node) { return node.tagName && node.tagName.toLowerCase() === lowerTagName; }),
children = (node ? node.childNodes : null);
return children;
}
ko.components.register("my-table", {
template: { element: 'my-table-template' },
viewModel: {
createViewModel: (params, componentInfo) => {
var a = {
files: params.files,
cellTemplateNodes: getChildNodes(componentInfo.templateNodes, 'cell-template')
};
return a;
}
},
});
Knockout doesn't check for this, but it expects the nodes passed to the template binding to be a true array. That's because the first thing it does is move the nodes to a new parent node. So you should copy the nodes to a new array:
return ko.utils.arrayPushAll([], children);
There's also a bug in Knockout 3.4.x when using the same node list multiple times, although it would cause a different effect. See https://github.com/knockout/knockout/issues/2187
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/
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>
I am trying to make a matrix matching table using knockout, but I'm stuck regarding the compare between the current data-bind and the parent data-bind.
Here is the jsfiddle.net/wup9rxeu/4/
<script type="text/html" id="cubeheader-template">
<th data-bind="text: $data.Name"></th>
</script>
<script type="text/html" id="body-template">
<!-- ko foreach: $parent.modeldim -->
<td>x</td>
<!-- /ko -->
</script>
What I want to accomplished is that when the table is populated with x and - for each td, based on the modelcubdim data.
I need some pointer on comparing the ID against the parent ID and if it's a match, then X or else -
Thanks
You can expand your model with transformed data to represent every cell in the table.
// just for easy searching items by its ID
data.itemById = function(arr, id){
return ko.utils.arrayFirst(arr, function(item){
return item.ID == id;
});
};
// the property that will hold actual data for *every* table row
// in the format { Name: [Cub Name], Data [array of "x" and "-"] }
data.series = ko.utils.arrayMap(data.modelcub, function(cub){
var cubdim = data.itemById(data.modelcubdim, cub.ID);
return {
Name: cub.Name,
Data: ko.utils.arrayMap(data.modeldim, function(dim){
var item = cubdim && data.itemById(cubdim.CubeDimension, dim.ID);
return item ? "x" : "-";
})
};
});
Then slightly change your markup:
<tbody data-bind="foreach: series">
<tr>
<th data-bind="text: Name"></th>
<!-- ko foreach: Data -->
<td data-bind="text: $data"></td>
<!-- /ko -->
</tr>
</tbody>
And you will get it working like here: http://jsfiddle.net/wup9rxeu/5/
From what I've been able to find online I don't think it's possible to use the foreach data-bind to iterate through the properties of an observable object in knockout at this time.
If someone could help me with a solution to what I'm trying to do I'd be very thankful.
Let's say I have an array of movies objects:
var movies = [{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
];
And what I would like to do is display this data in a table, but a different table for each genre of movie.
So I attempted something like this:
<div data-bind="foreach: movieGenre">
<table>
<tr>
<td data-bind="year"></td>
<td data-bind="title"></td>
<td data-bind="genre"></td>
</tr>
</table>
</div>
and my data source changed to look like this:
for (var i = 0; i < movies.length; ++i) {
if (typeof moviesGenres[movies.genre] === 'undefined')
moviesGenres[movies.genre] = [];
moviesGenres[movies.genre].push(movie);
}
I've tried about a dozen other solutions, and I'm starting to wonder if it's my lack of knowledge of knockout(I'm pretty green on it still), or it's just not possible the way I'd like it to be.
You can make your array "movies" an KO observable array and the array "movieGenre" a KO computed property. Have a look at this fiddle.
The code in the fiddle is given below for reader convenience;
KO View Model
function MoviesViewModel() {
var self = this;
self.movies = ko.observableArray([
{
title: 'My First Movie',
genre: 'comedy',
year: '1984'
},
{
title: 'My Next Movie',
genre: 'horror',
year: '1988'
},
{
title: 'My Other Movie',
genre: 'horror',
year: '1986'
}
]);
self.movieGenre = ko.computed(function() {
var genres = new Array();
var moviesArray = self.movies();
for (var i = 0; i < moviesArray.length; i++) {
if (genres.indexOf(moviesArray[i].genre) < 0) {
genres.push(moviesArray[i].genre);
}
}
return genres;
});
};
HTML
<div data-bind="foreach: movieGenre()">
<h3 data-bind="text: 'Genere : ' + $data"></h3>
<table border="solid">
<thead>
<tr>
<th>Title</th>
<th>Genre</th>
<th>Year</th>
</tr>
</thead>
<tbody data-bind="foreach: $parent.movies">
<!-- ko if: $data.genre === $parent -->
<tr>
<td data-bind="text: $data.title"></td>
<td data-bind="text: $data.genre"></td>
<td data-bind="text: $data.year"></td>
</tr>
<!-- /ko -->
</tbody>
</table>
</div>
As you can see "movieGenre" is made a computed property. Whenever the observable array "movies" changes, the moveGenre is calculated and cached. However, since this is not declared as a writable computed property, you cannot bind this to your view. Hence, it's value is used in the data binding.
The approach for rendering is simply looping through the calculated "movieGenre", and nest another loop for movies. Before adding a row to the table, for the corresponding table, the movie object is evaluated with the current movieGenre. Here, the container-less control flow syntax is used. We can use the "if" binding, but that would leave an empty table row per each movie object where the genre is otherwise.
The $parent binding context is used to access the parent contexts in the nested loop.
Hope this helps.