Unbinding ng-repeat scope - javascript

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'
}
};
});

Related

An animation in Angular to switch rows in a table

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;
});
});
}

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;
};
})

Angularjs evauate when data it ready

I am developing my first angular-app with an .Net backend.
I get my data async from a webmethod using a http.post. That all works fine.
Client-side I would like to do some simple calculations (a final row in a table which contains sums of all the data in table)
The code to do this is pretty straight forward but my problem is the data i not ready when I try to do it.
I have read that I could use a promise and a service or a factory. But I am not sure what we be the best way to go.
My code for the view:
<div ng-controller="taskCtrl as ctrl">
<div class="col-md-10 container outer">
<h1 class="center-block">{{ctrl.SprintViewModel.SprintName}}</h1>
<table id="SprintMetaDate">
<tr><td>Projekt:</td><td>{{ctrl.SprintViewModel.ProjektName}}</td></tr>
<tr><td>Periode:</td><td>{{ctrl.SprintViewModel.StartDate}} - {{Ctrl.SprintViewModel.EndDate}}</td></tr>
<tr><td>Udarbejdet af/d:</td><td>{{ctrl.SprintViewModel.MadeBy}}</td></tr>
</table>
<h3>Sprint Resume:</h3>
<br/>
{{ctrl.SprintViewModel.SprintResume}}
<h3>Sprint afslutning:</h3>
{{ctrl.SprintViewModel.SprintDemo}}
<h2>Scope og Økonomi </h2>
<h3>Sprint Opgaver</h3>
<table id="SprintTasks" class="col-md-12">
<tr><th>Opgave</th><th>Estimat</th><th>Forbrug</th><th>Udest.</th><th>*</th><th>Pris (DKK)</th></tr>
<tr ng-repeat="x in ctrl.SprintViewModel.Tasks">
<td style="width: 40%">{{ x.Description }}</td>
<td>{{ x.TimeEst }}</td>
<td>{{ x.TimeUsed }}</td>
<td>{{ x.TimeRemaining }}</td>
<td>{{ ctrl.CalcPrecisionOfEstimat(x.TimeUsed,x.TimeRemaining,x.TimeEst) | number:2}} %</td>
<td>{{x.Price}}</td>
</tr>
<tr>
<td>Ialt</td>
<td>{{ ctrl.TotalEstimat() }}</td>
<td>{{ ctrl.TotalTimeUsed() }}</td>
<td>{{ctrl.TotalTimeRemaining()}}</td>
<td>{{ctrl.TotalPrecision()}}</td>
<td>{{ctrl.TotalPrice()}}</td>
</tr>
</table>
* Forbrug + Udestående i forhold til estimat
<br/>
Udestående opgaver er planlagt ind i næstkommende sprint.
</div>
</div>
</form>
<script>
var app = angular.module('myApp', []);
app.controller('taskCtrl', function($scope, $http) {
var ctrl = this;
ctrl.SprintViewModel = null;
ctrl.TotalEstimat=function() {
var totalEstimat=0;
for (i=0; i<ctrl.SprintViewModel.Tasks.count;i++) {
totalEstimat += ctrl.SprintViewModel.Tasks[i].Estimate;
}
return totalEstimat;
}
ctrl.TotalPrecision = function () {
var totalPrecision=0;
angular.forEach(ctrl.SprintViewModel.Tasks, function (value, key) {
totalPrecision += Number(value);
});
$http.post('SprintRapport.aspx/GetSprintViewModel', {})
.then(function(response, status, headers, config) {
console.log("I success");
ctrl.SprintViewModel = response.data.d;
});
});`
As already mentioned I get a nullreference every when the page-load on all the methods in the last row, because ctrl.SprintviewModel is undefined. I have only included one of the methods for simplicity, the problem is the same for all of them.
So my question is how do I make sure that ctrl.TotalEstimat() first get called then ctrl.SprintViewModel is assigned?
You can add ng-if condition to the last <tr> which resolves to true when data is ready to populate in your controller. So you can define $scope.loading = false initially and once your code is ready to populate you set $scope.loading=true and that will call $digest cycle internally and your view gets updated.
There are several things that you could do. I've fixed this kind of issue by placing guard conditions in the functions. These check that the necessary variables have been set before continuing. So adding if (!ctrl.SprintViewModel) return; at the beginning of the function as follows:
ctrl.TotalEstimat=function() {
// Guard Condition to prevent function executing in invalid state.
if (!ctrl.SprintViewModel) return;
var totalEstimat=0;
for (i=0; i<ctrl.SprintViewModel.Tasks.count;i++) {
totalEstimat += ctrl.SprintViewModel.Tasks[i].Estimate;
}
return totalEstimat;
}
It's another option, but as you have already alluded to, I think that promises and the $q library is the proper angular way to fix this sort of thing.

nested tables from json object using anguarJS

Im trying to display a json object using angular. the object may have nested object or not, so i need to evaluate in runtime.
The logic is as follows :
for each key-val (k,v):
if v is an object then: for each (k2,v2) add another nested table in the HTML_TD. if not - need to print the value in a HTML_TD
The nested object works.
The problem : when v is an object i expect the expression to evaluates to '' and print nothing (because im going to an inner loop k2,v2) but instead, even if v is an object, it print it out to the HTML_TD
code :
<table border="1">
<tr ng-repeat="(k,v) in json.properties">
<td> {{ k }} </td>
<td> {{ typeof (v) == 'object' ? '' : v }} </td>
<td>
<table>
<tr ng-repeat="(k2,v2) in v">
<td> {{ k2 }} </td>
<td> {{ v2 }} </td>
</tr>
</table>
</td>
</tr>
The typeof function is not defined on angularjs template interpolation, the template syntax is a bit limited in terms of javascript functions but you can declare it on your scope like so:
$scope.typeof = function(obj) {
return typeof(obj);
};
So that a function named typeof will be visible on your scope and your snipet will be able to get working. The bellow snipet is a simple example implementing this solution.
angular.module('myApp', [])
.controller('myController', function($scope) {
$scope.json = {
properties: {
a: 'A',
b: 'B',
c: {
a1: 'A1'
}
}
};
$scope.typeof = function(obj) {
return typeof(obj);
};
});
angular.element(document).ready(function() {
angular.bootstrap(document, ['myApp']);
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<div ng-controller="myController">
<table border="1">
<tr ng-repeat="(k,v) in json.properties">
<td>{{ k }}</td>
<td>{{ typeof(v) == 'object' ? '' : v }}</td>
<td>
<table border="1">
<tr ng-repeat="(k2,v2) in v">
<td>{{ k2 }}</td>
<td>{{ v2 }}</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
I think angular parser don't allow typeof function in template.
But you can define one method in controller to get typeof.
E.g.:
$scope.getTypeof = function(value) {
return typeof value;
};
Now call this method in template code:
{{ getTypeof(v) === 'object' ? '' : v }}

conditional currency sign with angular filter

$scope.countries = [{name: 'UNITED KINGDOM', id: 1, sign:"£", 'val':2300987 },{name: 'AUSTRIA', id: 2, sign:"€", 'val':6703482 }
// In HTML
<tr ng-repeat="country in countries">
<td class="td-border">{{country.val| currency}}</td>
</tr>
I need angular filter which will take care of currency conditionally as per above object..
From Angular Docs
Filter definition {{ currency_expression | currency : symbol :
fractionSize}}
Simply you could add parameter to filter which would be sign of your currency.
Markup
<tr ng-repeat="country in countries">
<td class="td-border">{{country.val| currency: country.sign}}</td>
</tr>
I believe you need to filter the rows based on currency. You can achieve that by simply updating the code to following
// In HTML
<tr ng-repeat="country in countries | filter : {sign : currency}">
<td class="td-border">{{country.val}}</td>
</tr>
First, if you want the filter to format your currency you will have to pass the country object or the sign and the val to it, otherwise you can't add the sign with the filter.
.filter('currency', [function() {
return function(countryObj) {
return countryObj.val + countryObj.sign;
}
};
}])
The filtering of angular could be bind on the repeater.
Using a separate model to store the current currency is super easy:
$scope.currency = "£";
Filtering on the repeater will be quite easy then:
<tr ng-repeat="country in countries | filter : { sign : currency || ''}">
<td class="td-border">{{country.val}}</td>
</tr>
Working demo: http://jsfiddle.net/CBgcP/692/

Categories