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
Related
I have a user table generated with data from an ajax request. Table roughly looks like this: [Image of Table][1]
When an admin edits a user's username, I want the user's row to re-render with the changes (specifically the users firstname and lastname).
I create the table fine. The models work fine. My parent component receives the edited data just fine in my edit() method, but I can't seem to make my target row re-rendered with my changed data. How can I make my target row update?
I tried the following and it didn't work:
How to update a particular row of a vueJs array list?
https://v2.vuejs.org/v2/guide/list.html#Caveats
I have set key to my row
Tried setting my listOfUsers with Vue.set()
Tried using Vue.set() in place of splice
Here is my parent vue component with the following (I took out irrelevant details):
TEMPLATE:
<table>
<thead>
<tr>
<th>Name</th>
<th>Email Address</th>
<th>Created</th>
<th>Stat</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in listOfUsers" :key="'row' + user._id">
<td>{{user.first_name + ' ' + user.last_name}}</td>
<td>{{user.email}}</td>
<td>{{user.created}}</td>
<td>
<a v-if="user.confirmed" #click="determineButtonClicked(index, 'confirm')"></a>
<a v-else #click="determineButtonClicked(index, 'unconfirm')"></a>
</td>
<td class="buttonCase">
<a #click="determineButtonClicked(index, 'info')"></a>
<a v-if="user.blocked" #click="determineButtonClicked(index, 'blocked')"></a>
<a v-else #click="determineButtonClicked(index, 'block')"></a>
<a v-if="user.enforce_info === 'required'" #click="determineButtonClicked(index, 'enforceInfoActive')"></a>
<a v-else-if="user.enforce_info === 'done'" #click="determineButtonClicked(index, 'enforceInfoChecked')"></a>
<a v-else #click="determineButtonClicked(index, 'enforceInfo')"></a>
<modal v-if="usersList[index]" #toggleClickedState="setState(index)" #editUser="edit(index, $event)" :id="user._id" :action="action"></modal>
</td>
</tr>
</tbody>
</table>
SCRIPT
<script>
export default {
created: function() {
let self = this;
$.getJSON("/ListOfUsers",
function(data){
self.listOfUsers = data;
});
},
data: function() {
return {
listOfUsers: [],
}
},
methods: {
edit(index, update){
let user = this.listOfUsers[index];
user.firstName = update.firstName;
user.lastName = update.lastName;
// this.listOfUsers.splice(index, 1, user)
this.listOfUsers.$set(index, user)
}
}
}
</script>
Thank you for your time and help!
[1]: https://i.stack.imgur.com/lYQ2A.png
Vue is not updating in your edit method because the object itself is not being replaced. Properties of the object do change, but Vue is only looking for the object reference to change.
To force the array to detect a change in the actual object reference, you want to replace the object, not modify it. I don't know exactly how you'd want to go about doing it, but this hacked together fiddle should demonstrate the problem so you can work around it: http://jsfiddle.net/tga50ry7/5/
In short, if you update your edit method to look like this, you should see the re-render happening in the template:
methods: {
edit(index, update){
let currentUser = this.listOfUsers[index];
let newUser = {
first_name: update.firstName,
last_name: update.lastName,
email: currentUser.email,
created: currentUser.created
}
this.listOfUsers.splice(index, 1, newUser)
}
}
You can have a try like this
<script>
export default {
created: function() {
let self = this;
$.getJSON("/ListOfUsers",
function(data){
self.listOfUsers = data;
});
},
data: function() {
return {
listOfUsers: [],
}
},
methods: {
edit(index, update){
let user = this.listOfUsers[index];
user.firstName = update.firstName;
user.lastName = update.lastName;
// this.listOfUsers.splice(index, 1, user)
this.$set(this.listOfUsers,index, user)
}
}
}
</script>
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>
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'm in the process of learning Polymer. I am trying to bind an array to my UI. Each object in the array has a property that will change. I need my UI to update when the property value changes. I've defined my Polymer component as follows:
my-component.html
<dom-module id="my-component">
<template>
<h1>Hello</h1>
<h2>{{items.length}}</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr repeat="{{ item in items }}">
<td>{{ item.name }}</td>
<td>{{ item.status }}</td>
</tr>
</tbody>
</table>
<br />
<button on-click="testClick">Test</button>
</template>
<script>
// element registration
Polymer({
is: "my-component",
properties: {
items: {
type: Array,
value: function() {
return [
new Item({ name:'Tennis Balls', status:'Ordered' }),
new Item({ name:'T-Shirts', status: 'Ordered' })
];
}
}
},
testClick: function() {
for (var i=0; i<items.length; i++) {
if (items.name === 'Tennis Balls') {
items[i].status = 'Shipped';
break;
}
}
}
});
</script>
</dom-module>
The component renders. However, the bindings do not work at all.
The line with {{ items.length }} does not show a count. Its basically just an empty h2 element.
The first item gets rendered in the list. However, the second one does not.
When I click the Test button, the update to the status is not reflected on the screen.
When I look at everything, it looks correct to me. However, it is clear from the behavior that the data-binding is not setup properly. What am I doing wrong? The fact that items.length and the initial rendering of all of the items in the array has me really confused.
Polymer data binding system works like this:
If the declared property changes (for example adding a new item) then it will detect the change and update your DOM.
However Polymer won't monitor changes inside your property (For performance/compatibility reasons).
You need to notify Polymer that something inside your property changed. You can do that using the set method or notifyPath.
E.g (From the polymer data binding section)
this.set('myArray.1.name', 'Rupert');
You can also add an observer if you want to do something when your array is updated.
Polymer 1.0 properties Documentation
And I think you should also add a notify:true to your property
items: {
type: Array,
notify:true,
value: function() {
return [
new Item({ name:'Tennis Balls', status:'Ordered' }),
new Item({ name:'T-Shirts', status: 'Ordered' })
];
}
}
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/