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' })
];
}
}
Related
In a Svelte app, I have this array of countries:
let countries = [
{
name:"Alegeria",
status: "1"
},
{
name:"Bulgaria",
status :"0"
}
]
Note the status property is a string. I iterate the array this way:
{#if countries.length > 0}
<table class="table">
<thead>
<tr>
<th>Country</th>
<th class="text-right">Status</th>
</tr>
</thead>
<tbody>
{#each countries as c}
<tr>
<td>{c.name}</td>
<td class="text-right"><Switch bind:checked={Boolean(Number(c.status))} /></td>
</tr>
{/each}
</tbody>
</table>
{:else}
<p class="alert alert-danger">No countries found</p>
{/if}
As you can see, I try to convert the value of the status property to a boolean this by using Boolean(Number(c.status)).
Instead of the desired conversion I get the error: Can only bind to an identifier (e.g. foo) or a member expression as the REPL shows.
What am I doing wrong?
As it says in the error, you can only bind to an identifier or member expression - ie a variable.
This is because a bind is a two-way thing, and if you have applied Boolean(Number(()) to it, when someone changes that input, then svelte doesn't know how to undo those functions to 'save' the data back into that variable it's bound to.
If you can't change the status variables to be boolean (better solution, as suggested by other answers), you need to manually do this two-way updating. Drop the bind, just have checked={Boolean(Number(c.status))}, and handle the input's change event to convert from true/false back into "1" or "0", and save that to the status.
Use:
function handleClick(country) {
countries.find(c => c.name == country.name).status = (country.status == "1") ? "0" :"1"
}
and
<Switch checked={Boolean(Number(c.status))} on:change={() => handleClick(c)}/>
See it working in this repl
I think the problem is that the Boolean() function creates a new object, to which you can't bind, because it is never again referenced. You can bind directly to your array of values in countries, using this code:
{#each countries as c, index}
<tr>
<td>{c.name}</td>
<td class="text-right"><Switch bind:checked={countries[index].status} /></td>
</tr>
{/each}
What has changed is that you use the index parameter of the #each loop now to bind to the variable of the countries array. Please be aware that in order for this to properly work, you need to change the status values to true or false. Otherwise it will still work, but the initial value will always be true.
If you just want pass the value down to the Switch component, simply remove the bind: like so:
<td class="text-right"><Switch checked={Boolean(Number(c.status))} /></td>
If you want to update the countries model via the switch component, I suggest to forward the click event and use a simple click handler method, something like this:
function onClick(event, country) {
countries = countries.map(c => {
if (c.name === country.name) {
c.status = event.target.checked ? '1' : '0';
}
return c;
})
}
...
<td class="text-right"><Switch checked={c.status === '1'} on:click={(e) => onClick(e, c)}/></td>
full code on REPL: https://svelte.dev/repl/013286229d3847c1895c4977aee234af?version=3.9.1
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 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>
I have table generated with dynamic ids like this one
<table>
<tbody>
<tr *ngFor="let row of createRange(seats.theatreDimension.rowNum)">
<td id={{row}}_{{seat}} *ngFor="let seat of createRange(seats.theatreDimension.seatNumInRow)">
</td>
</tr>
</tbody>
</table>
I want to access table td element from angular 2 .ts file. Here are functions:
ngOnInit() {
this.getSeats();
}
getSeats() {
this.cinemaService.getAllSeats(this.repertoire.id).subscribe(
data => {
this.seats = data.json();
this.setReservedSeats();
}
)
}
setReservedSeats() {
this.seats.reservedSeats.forEach(seat => {
let cssSeatId = seat.rowNumReserved + "_" + seat.seatNumInRowResereved;
document.getElementById(cssSeatId).className += "reserved";
}
)
}
and after that I want dynamically to set class of every td, but I am getting this console error in my browser:
ERROR TypeError: Cannot read property 'className' of null
Just to note once again that I generate td ids dynamically. They are in form rowNum_cellNum.
I am getting this object from api.
{
"theatreDimension": {
"rowNum": 17,
"seatNumInRow": 20
},
"reservedSeats": [
{
"rowNumReserved": 9,
"seatNumInRowResereved": 19
},
{
"rowNumReserved": 2,
"seatNumInRowResereved": 4
},
{
"rowNumReserved": 15,
"seatNumInRowResereved": 15
}
]
}
I am using theatreDimension to make table. Then I try to make reserved seats from reservedSeats array with red background (reserved)
How I can access td elements from angular 2 and solve this issue?
Instead of accessing the DOM directly, you should try using the ngClass directive to set the class:
<td [ngClass]="{'reserved': isReserved(row, seat)}" id={{row}}_{{seat}} *ngFor="let seat of createRange(seats.theatreDimension.seatNumInRow)">
</td>
You can then implement the isReserved(row, seat) function, and if it returns true, it will apply the reserved class.
isReserved(rowNum: number, seatNum: number) {
return this.seats.reservedSeats.some((r) => r.rowNumReserved === rowNum && r.seatNumInRowResereved === seatNum);
}
To directly answer your question, in order to get the element by ID, you need to do so after the page has been renedered. Use the function ngAfterViewInit(). You will need to remove the call to setReservedSeats() from getSeats().
ngOnInit() {
this.getSeats();
}
ngAfterViewInit(){
this.setReservedSeats();
}
However, I would suggest going a different route. You can set the style of the element based on whether or not the seat has been reserved. Assuming you have some sort of "reserved" flag on the seat object you can do something like this:
<td id={{row}}_{{seat}}
[ng-class]="{'reserved' : seat.researved}"
*ngFor="let seat of createRange(seats.theatreDimension.seatNumInRow)">
</td>
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.