An animation in Angular to switch rows in a table - javascript

I have an HTML file that displays a voting chart for musical songs using Angular. Each time I press the refresh button, the chart data is updated by making a GET request. I am trying to create an animation that shows the changes in the table when the data is updated. For example, when the third most voted track becomes the first most voted track after the update, I want the third row to visually move up in the table and the first and second rows to move down. Can someone help me figure out how to achieve this animation?
<button onclick="reloadData()">REFRESH</button>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Track</th>
<th>Artist</th>
<th>Percentage</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let track of votedTracks; let i = index">
<td>{{ i + 1 }}</td>
<td class="track-name">{{ track.name }}</td>
<td class="artist-name">{{ track.artist }}</td>
<td class="percentage">{{ track.percentage | number : "1.0-2" }}%</td>
</tr>
</tbody>
</table>
I have tried to create a custom animation using the Angular animation module, but the rows are not moving as expected. I'm expecting the rows to move up or down in the table when the data is updated and the ranking of the tracks changes. Last time it showed only the first row and than it gave me the error:
> ERROR Error: Unexpected synthetic property #moveRow found. Please make sure that:
> Either `BrowserAnimationsModule` or `NoopAnimationsModule` are imported in your application.
> There is corresponding configuration for the animation named `#moveRow` defined in the `animations` field of the `#Component` decorator (see https://angular.io/api/core/Component#animations).

Import the BrowserAnimationsModule in your app.module.ts file.
import { BrowserAnimationsModule } from '#angular/platform-browser/animations';
You also need to add [#moveRow] to the <tr>, like so:
<tr *ngFor="let track of votedTracks; let i = index" [#moveRow]>
All together it would something like this:
#Component({
animations: [
trigger('moveRow', [
state('in', style({transform: 'translateY(0)'})),
transition('void => *', [
style({transform: 'translateY(-100%)'}),
animate(200)
]),
transition('* => void', [
animate(200, style({transform: 'translateY(100%)'}))
])
])
],
// ... etc
})
<tbody>
<tr *ngFor="let track of votedTracks; let i = index" [#moveRow]="i">
<td>{{ i + 1 }}</td>
<td class="track-name">{{ track.name }}</td>
<td class="artist-name">{{ track.artist }}</td>
<td class="percentage">{{ track.percentage | number : "1.0-2" }}%</td>
</tr>
</tbody>
Then in your reloadData() method you need to use ngZone.run() method because the change detection doesn't run automatically from an async data source... you would have something like this:
reloadData() {
this.http.get('/api/voted-tracks').subscribe(data => {
this.ngZone.run(() => {
this.votedTracks = data;
});
});
}

Related

How can I populate the dropdown values for each row within a data table?

The following is my HTML.
<tr>
<th>
Test Data
</th>
<th>
Test Data Options
</th>
</tr>
<tr>
<td>
<ng-container>
{{data.testData}}
</ng-container>
</td>
<td class="text-base font-normal">
<ng-container>
<p-dropdown [options]="dropdownOptions" placeholder="Select Test Data"></p-dropdown>
</ng-container>
</td>
</tr>
This is my JSON file.
“data”: [
{
“testData”: “Test Data A”,
“testDataOptions”: [
{
“testDataOptionValue”: “Test Data A1 Option”,
“testDataOptionLabel”: “Test Data A1”
},
{
“testDataOptionValue”: “Test Data A2 Option”,
“testDataOptionLabel”: “Test Data A2”
}
],
},
{
“testData”: “Test Data B”,
“testDataOptions”: [
{
“testDataOptionValue”: “Test Data B1 Option”,
“testDataOptionLabel”: “Test Data B1”
},
{
“testDataOptionValue”: “Test Data B2 Option”,
“testDataOptionLabel”: “Test Data B2”
}
],
}
How could I populate the dropdown values in each row in the data table according to their indexes?
For example
The Test Data A row dropdown values should be:
Test Data A1, Test Data A2
The Test Data B row dropdown values should be:
Test Data B1, Test Data B2
I guess you're missing a *ngFor in the code snippet you sent:
<tr *ngFor="let data of json.data">
<td>{{ data.testData }}</td>
<td>
<p-dropdown [options]="data.testDataOptions" optionLabel="testDataOptionLabel" optionValue="testDataOptionValue"></p-dropdown>
</td>
</tr>
Edit: binding the selected value
A first option, although it might feel hacky depending on context, is to bind to the data object you already have. For instance:
<p-dropdown [options]="data.testDataOptions" optionLabel="testDataOptionLabel" optionValue="testDataOptionValue" [(ngModel)]="data.selectedValue"></p-dropdown>
If that feels weird or you just don't want to mess with that data, then you'll need to store it in a separate variable, which can be an array or a key/value structure. Then you just bind each item to the corresponding entry in the the data structure.
Allan gave you the correct answer, I'm just going to expand it a bit. Since your dropdown options list might be a large one, please consider using trackBy as well. It is not mandatory and it does not always prove much more performant, but overall it should help with performance. You can do it like so:
<tr *ngFor="let data of json.data; trackBy: trackByMethod">
<td>{{ data.testData }}</td>
<td>
<p-dropdown [options]="data.testDataOptions" optionLabel="testDataOptionLabel" optionValue="testDataOptionValue"></p-dropdown>
</td>
</tr>
While in your .ts file (make sure you add some IDs to your items as well - or use other unique identifiers):
trackByMethod = (index: number, item: any): string => item.id;
You can read more about trackBy here: https://dev.to/rfornal/angular-is-trackby-necessary-with-ngfor-594e

ng-zorro antd, nz-table dynamic table columns and rows

I have a table where columns and row cells are dynamically set,
in the table header th content should be dynamic and also for table body tr maybe contain HTML that contains another component tag.
is there any way to handle that, I have created a component called table and this table has #Input and #Output to be reusable for different usage.
in the ng-zorro documentation, there is no way to use the table data source technique so I can use render functions like react and.
You can create two inputs, one for the columns and one for the rows.
To make the columns dynamically you have to send to the column input an array with column objects. There you can set everything that you want. I usually use the tittle and the column function like that:
listOfColumn = [
{
title: 'Code',
compare: (a: User, b: User) => a.code.localeCompare(b.code)
},
{
title: 'Customer',
compare: (a: User, b: User) => a.name.localeCompare(b.name)
}
]
The html code to use it is the following:
<thead>
<tr>
<th *ngFor="let column of listOfColumn" [nzSortFn]="column.compare">{{ column.title }}</th>
</tr>
</thead>
And for the data, just send it to other input and set the array as data input for the table and make a loop to display the content:
<nz-table
#basicTable
[nzData]="data">
<thead>
<tr>
<th *ngFor="let column of listOfColumn" [nzSortFn]="column.compare">{{ column.title }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of basicTable.data">
<td>{{ user.code }}</td>
<td>{{ user.name }}</td>
</tbody>
</nz-table>
I hope I answer your question :D

AngularJS How Can I Ignore hidden table rows in ng-repeat but keep the increment?

I have an ng-repeat that generates a series of table rows. One column is titled "task status" and if the status is displaying "done", I see no reason in showing it, as the job has been completed.
I used ng-show = values != 0; this initially worked until I added an increment to number the tasks.
What I have found was that the data = "done" were not totally removed from the DOM and still regstering in the list disrupting the increment. See image below:
list increment disruption
So the rows 2 and 3 are data that equal "done". What can I do to ignore them?
Here is my markup:
<table class="backlog table table-bordered table-striped" width="100%" border="0" cellpadding="0" cellspacing="0" summary="Our Jira Backlog">
<tbody>
<tr>
<th>Priority</th>
<th>Task Priority Score</th>
<th>Task Summary</th>
<th>Due date</th>
<th>Task Status</th>
</tr>
<tr ng-repeat="issue in issues | orderBy: '-fields.customfield_12401'" ng-if="issue.fields.status.statusCategory.name != 'Done'">
<td>{{ $index + 1 }}</td>
<td>{{ issue.fields.customfield_12401 }}</td>
<td>{{ issue.fields.summary }}</td>
<td>{{ issue.fields.customfield_13700 }}</td>
<td>{{ issue.fields.status.statusCategory.name }}</td>
</tr>
</tbody>
</table>
So anything that comes from "issue.fields.status.statusCategory.name" needs to be ignored so the Priority (First Column) goes, 1,2,3,4,5 etc and not display "done" the Task Status Column.
I think the best way to handle this situation is to the filter the array first. You can filter the array all in the ng-repeat expression, just retain a variable of the filter output that you can reference in the template (if you need to).
Check out the answer to this SO question: AngularJS - how to get an ngRepeat filtered result reference
edit: to clarify I think you should change your ng-if into a custom filter, and apply it before the ng-repeat indexes the filtered array:
<tr ng-repeat="issue in (filteredIssues = (issues | orderBy: '-fields.customfield_12401' | filter: customFilterFunction))">
<td>{{ $index + 1 }}</td>
You should be able to keep your ng-if as a filter and also keep incrementing your $index by simply moving your $index to the ng-repeat as track by:
<tr ng-repeat="issue in (issues | orderBy: '-fields.customfield_12401') track by $index" ng-if="issue.fields.status.statusCategory.name != 'Done'">
Your $index should match the index of the for-each being performed, even with the ng-if implemented.
UPDATE: I believe I interpreted your question wrong. You want your $index to skip you "done" rows? That wasn't clear...
As #Ericson578 suggested, you should make a custom filter to remove the "done" rows from your array.
<tr ng-repeat="issue in ((issues | removeDoneRows) | orderBy: '-fields.customfield_12401') track by $index" >
...and your removeDoneRows filter (please come up with a better name for this):
angular.module('myFilter', [])
.filter('removeDoneRows', function() {
return function(arr) {
var retval = [];
for(var i = 0; i < arr.length; i++){
if(arr[i].fields.status.statusCategory.name.toLowerCase() != "done")
retval[retval.length] = arr[i];
}
return retval;
};
})

ng-repeat takes too much time to render data

I know there are many questions already posted for the same issue but none of the solutions work in my case.
On calling a web service I get JSON response. In this JSON, there are around 2000+ objects from which I need to display data on the table. I want to display all (2000+) records in the table and Yes, I cannot limit or paginate, need to display it on a single page (I know it's stupid but it's the business requirement). I don't need sorting or searching.
Data transfer is about 2MB and the request completes in about 2-4 secs approx. but it takes around 10-15 secs to data render on the page.
Now, what I am looking for is either speed ng-repeat binding things up (if possible) or display the data as soon as I receive it and keep on adding it until all rows are displayed.
Check out the code below :
HTML
<table class="table table-bordered table-striped cf">
<thead style="color: #333;">
<tr>
<td>Asset Name</td>
<td>Date/ Time</td>
<td>Location</td>
<td>Ignition</td>
<td>Speed</td>
<td>Heading</td>
<td>Direction</td>
</tr>
</thead>
<tbody>
<tr ng-repeat="cols in tableData">
<td>{{ cols.aN }}</td>
<td>{{ cols.dT }}</td>
<td>{{ cols.Lat }}, {{ cols.Lon }}</td>
<td>{{ cols.I }}</td>
<td>{{ cols.S }}</td>
<td>{{ cols.H }}</td>
<td>{{ cols.D }}</td>
</tr>
</tbody>
</table>
JS
var ignition_text = '';
var lat = '';
var lon = '';
for (var i = 0; i < data.length; i++) {
if (data[i].ignition = 1) {
ignition_text = "On";
} else {
ignition_text = "Off";
}
$scope.$apply(function() {
$scope.tableData.push({
aN: name,
dT: data[i].eventUTCTime,
Lat: data[i].latitudeDegrees,
Lon: data[i].longitudeDegrees,
I: ignition_text,
S: data[i].speedMPH,
H: data[i].longitudeDegrees,
D: data[i].latitudeDegrees
});
});
}
Thanks in advance!
You probably wont need $scope.$apply at all. And even if you need it, you should only use it once you pushed all data to the table. Otherwise, every added entry will force an digest-cycle. Just build your array and assign the finished array to the scope-variable. Then angular will only build the table once.
Depending on the nature of your variable name you may be able to eliminate the array building as well and just use the data you are downloading. Apart from nameyou just use that data anyway.
Here is a plunk that has a similar data size but loads much faster http://plnkr.co/edit/I4rN1ZMaR3e1mbcsJ9Ka. If you were to make a quick plunk I could use your data and edit your code but from the looks you just need the main assignment to the scope without the apply for the data and add a track by to the ng-repeat. SN: You would want to manipulate your data inside the for loop then do the assignment to the scope.
for (var i = 0; i < data.length; i++) {
if (data[i].ignition = 1) {
ignition_text = "On";
} else {
ignition_text = "Off";
}
}
$scope.tableData=data;
JS
$http.get("largeData.json").then(function(response) {
vm.test = response.data;
});
HTML
<tbody>
<tr ng-repeat="(key, value) in main.test track by $index ">
<td>{{ value.ask }}</td>
<td>{{ value.bid }}</td>
<td>{{ value.volume_btc }}, {{ value.volume_percent }}</td>
<td>{{ value.last }}</td>
<td>{{ value.timestamp }}</td>
</tr>
</tbody>

Unbinding ng-repeat scope

I have two <tr>s and an ng-repeat on each, but both perform the same operation on their child elements, like so:
<tr ng-repeat="item in data : filterFunc" ng-if="mode === 'something'">
<td>{{item.name}}</td>
</tr>
<tr ng-repeat="item in data : filterFunc" ng-if="mode === 'somethingelse'">
<td>{{item.name}}</td>
</tr>
Imagine, there are two types of data sets and which one is to be rendered is decided by the mode property at run time. So, either the first <tr> is rendered in the DOM or the other one.
Initially the first <tr> will be rendered and the associated filterFunc function will work properly. There's a simple drop down which has two options, one each for the two modes. If you select the other mode, the second <tr> will be rendered and first one will be removed from the DOM.
The problem is, the filterFunc now is bound to both the <tr>s and operates on both of them.
How do I unbind the scope or watchers for the first one let it be bound only to the second one? or any one of them at any point of time? Here's the fiddle http://jsfiddle.net/6kx4ojL4/
Note: For the sake of simplicity, I have simply returned the passed-in data object as-is in filterFunc. Check the output in browser console. It gets called twice, i.e. for both data sets.
It is not clear what you want to do exactly from your description. My first thought is that you could separate your modes in ng-if, say, use mode1 and mode2, so you can control two <tr>s separately..
Use ng-show instead of ng-if
<tr ng-repeat="item in data : orderBy: 'name'" ng-show="mode === 'something'">
<td>{{item.name}}</td>
</tr>
<tr ng-repeat="item in data : orderBy: 'name'" ng-show="mode === 'somethingelse'">
<td>{{item.name}}</td>
</tr>
How about this. Check this JSFiddle: http://jsfiddle.net/vxcjw45d/1/
If it's not what are you looking for tell me - I will delete it :)
HTML:
<body ng-app="myApp">
<table ng-controller="myController">
<tr ng-repeat="item in data | orderBy: 'name'"
ng-if="mode === 'something'">
<td>{{ item.name }}</td>
</tr>
<tr ng-repeat="item in data | orderBy: 'name'"
ng-if="mode === 'somethingelse'">
<td>{{ item.age }}</td>
</tr>
<tr>
<td>
<button ng-click="changeMode()">
Change Mode
</button>
</td>
</tr>
</table>
</body>
JS:
var myApp = angular.module('myApp', []);
myApp.controller('myController', function($scope) {
$scope.data = [
{ name: 'John', age: 21 },
{ name: 'Doe', age: 33 }
];
$scope.mode = 'something';
$scope.changeMode = function() {
if ($scope.mode === 'something') {
$scope.mode = 'somethingelse';
} else if ($scope.mode === 'somethingelse') {
$scope.mode = 'something'
}
};
});

Categories