Invert Angular 2 *ngFor

<li *ngFor="#user of users ">
{{ user.name }} is {{ user.age }} years old.
</li>

Is it possible to invert the ngFor that the items are added bottom up?

78472 次浏览

Update

@Pipe({
name: 'reverse',
pure: false
})
export class ReversePipe implements PipeTransform {
constructor(private differs: IterableDiffers) {
this.differ = this.differs.find([]).create();
}


transform(value) {
const changes = this.differ.diff(value);
if (changes) {
this.cached = value.slice().reverse();
}
return this.cached;
}
}

By default, Angular only calls the pipe's transform() method when the array reference has been changed, so it won't be called when items are added or removed from/to the array, meaning that changes to the array won't be reflected in the UI.

For the pipe to get called every time change detection is called, I made the pipe impure. An impure pipe will be called very often, therefore it's important for it to work efficient. Creating a copy of an array (perhaps even a large array) and then reversing its order is quite expensive.

Therefore a differ is to only do the actual work if some changes were recognized and otherwise return the cached result from the previous call.

Original

You can create a custom pipe that returns the array in the reverse order or just provide the data in the reverse order in the first place.

See also

You need to implement a custom pipe that leverage the reverse method of JavaScript array:

import { Pipe, PipeTransform } from '@angular/core';


@Pipe({ name: 'reverse' })


export class ReversePipe implements PipeTransform {
transform(value) {
return value.slice().reverse();
}
}

You can use it like that:

<li *ngFor="let user of users | reverse">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

Don't forget to add the pipe in the pipes attribute of your component.

There are two problems with the selected answer:

First, the pipe won't notice source array modification, unless you add pure: false to the pipe's properties.

Second, the pipe does not support two-directional binding, because the copy of the array is reversed, not the array itself.

The final code looks like:

    import {Pipe} from 'angular2/core';


@Pipe({
name: 'reverse',
pure: false
})
export class ReversePipe {
transform (values) {
if (values) {
return values.reverse();
}
}
}

Plunker

http://plnkr.co/edit/8aYdcv9QZJk6ZB8LZePC?p=preview

You can simply use JavaScript's .reverse() on the array. Don't need an angular specific solution.

<li *ngFor="#user of users.slice().reverse() ">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

See more here: https://www.w3schools.com/jsref/jsref_reverse.asp

Using transform requires PipeTransform:

    import { Pipe, PipeTransform } from '@angular/core';


@Pipe({
name: 'reverse',
pure: false
})
    

export class ReversePipe implements PipeTransform {
transform (values: any) {
if (values) {
return values.reverse();
}
}
}

Call it with <div *ngFor="let user of users | reverse">

This should do the trick

<li *ngFor="user of users?.reverse()">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

If anyone is using typescript here is how I managed to get it to work, using @Günter Zöchbauer's answer.

@Pipe({
name: 'reverse',
pure: false
})
export class ReversePipe implements PipeTransform {


differs: any
cashed: any
constructor(private _differs: IterableDiffers) {
this.differs = this._differs.find([]).create(null);
}
/**
* Takes a value and makes it lowercase.
*/
transform(value, ...args) {
const changes = this.differs.diff(value)
if (changes) {
this.cashed = value.slice().reverse();;
};
return this.cashed
}
}

One could use ngx-pipes

npm install ngx-pipes --save

module

import {NgPipesModule} from 'ngx-pipes';


@NgModule({
// ...
imports: [
// ...
NgPipesModule
]
})

component

@Component({
// ..
providers: [ReversePipe]
})
export class AppComponent {
constructor(private reversePipe: ReversePipe) {
}
}

then using in template <div *ngFor="let user of users | reverse">

you can do it using javascript function.

<li *ngFor="user of users?.slice()?.reverse()">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

The solution of using .slice().reverse() in the template is simple and effective. However, you should be careful when using the iteration index.

This is because .slice() method returns a new array for the view, so that our initial array does not change. The problem is that the reversed array has also reversed index.

Therefore, you should also reverse the index, if you want to use it as a variable. Example:

<ul>
<li *ngFor="let item of items.slice().reverse(); index as i" (click)="myFunction(items.length - 1 - i)">\{\{item}}</li>
</ul>

In the above, instead of i we use the reversed items.length - 1 - i so that a clicked item corresponds to the same item of our initial array.

Check this stackblitz: https://stackblitz.com/edit/angular-vxasr6?file=src%2Fapp%2Fapp.component.html

try this:

<ul>
<li *ngFor="let item of items.slice().reverse()">\{\{item}}</li>
</ul>

Surprisingly, after so many years there is still no solution posted here that does not have serious flaws. That is why in my answer, I am going to explain what drawbacks all current solutions have and present two new approaches to this problem which I consider to be better.


Drawbacks of current solutions

Live demos are available for each solution. Please check out the links under the explanations.

1. Pure pipe with slice().reverse() (currently accepted)

— by @Thierry Templier and @rey_coder (since ngx-pipes uses this method)

@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
transform(value) {
return value.slice().reverse();
}
}

Pipes are pure by default, so Angular expects them to produce the same output given the same input. That is why the transform() method will not be called again when you make changes to the array unless you create a new array every time you want to update the old one, for example by writing

this.users = this.users.concat([{ name, age }]);

instead of

this.users.push({ name, age });

but doing so just because your pipe requires it is a bad idea.

So this solution only works if you are not going to modify the array.

In the live demo, you will see that only the Reset button works because it creates a new (empty) array when clicked, whereas the Add button only mutates the existing array.

Live demo

2. slice().reverse() without a pipe

— by @André Góis, @Trilok Singh and @WapShivam

<li *ngFor="let user of users.slice().reverse()">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

Here, we use the same array methods but without a pipe, which means that Angular will call them on each change detection cycle. Changes to the array will now be reflected in the UI, but the problem here is that slice() and reverse() might get called too often because array changes are not the only thing that can cause change detection to run.

Note that we get the same behavior if we modify the code of the previous solution to make the pipe impure. We can do so by setting pure: false in the object passed to the Pipe decorator. I did it in the live demo in order to be able to log something in the console whenever slice() and reverse() are going to get called. I also added a button that lets you trigger change detection without changing the array. After clicking it, you will see in the console that slice() and reverse() were called although the array had not been modified, and that is something we would not want in an ideal solution. Nevertheless, this solution is the best of all current solutions.

Live demo

3. Trying to improve performance by caching the reversed array

— by @Günter Zöchbauer

@Pipe({ name: 'reverse', pure: false })
export class ReversePipe implements PipeTransform {
constructor(private differs: IterableDiffers) {
this.differ = this.differs.find([]).create();
}


transform(value) {
if (this.differ.diff(value)) {
this.cached = value.slice().reverse();
}
return this.cached;
}
}

This is a clever attempt to solve the problem I described in the previous section. Here, slice() and reverse() are only called if the array's contents have actually changed. If they have not changed, the cached result of the last operation is returned.

To check for array changes, an Angular's IterableDiffer object is used. Such objects are also used by Angular internally during change detection. Again, you can check the console in the live demo to see that it actually works.

However, I do not think there is any performance improvement. On the contrary, I think this solution makes it even worse. The reason is that it takes the differ linear time to find out that the array has not changed, which is just as bad as calling slice() and reverse(). And if it has changed, the time complexity is still linear in the worst case because in some cases, the entire array has to be traversed before the differ detects a change. As an example, this happens when the last element of the array has been modified. In that case, the entire array will have to be traversed twice: when calling the differ and when calling slice() and reverse(), which is just terrible.

Live demo

4. Only using reverse()

— by @Prank100 and @Dan

<li *ngFor="let user of users.reverse()">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

This solution is an absolute no go.

The reason is that reverse() mutates the array instead of creating a new one, which is most probably undesired because you might need the original one in a different place. If you don't, you should just reverse the array as soon as it arrives in your application and never have to worry about it again.

There is an even more significant problem, which is especially dangerous because you cannot see it in development mode. In production mode, your array will be reversed during each change detection cycle, meaning that an array like [1,2,3] will keep switching between [1,2,3] and [3,2,1]. You definitely don't want that.

You don't see it in development mode because in that mode, Angular performs additional checks after each change detection run, which cause the reversed array to be reversed one more time bringing it back in its original order. The side effects of the second check are not reflected in the UI, and that is why everything seems to work fine, although in reality, it does not.

Live demo (production mode)


Alternative solutions

1. Only using array indices

// In the component:
userIdentity = i => this.users[this.users.length - 1 - i];
<li *ngFor="let _ of users; index as i; trackBy:userIdentity">
\{\{ users[users.length - 1 - i].name }} is \{\{ users[users.length - 1 - i].age }} years old.
</li>

Instead of grabbing the user, we only grab the index i and use it to get the ith last element of the array. (Technically, we grab the user too, but we use _ as the variable name to indicate that we are not going to use that variable. That is a pretty common convention.)

Having to write users[users.length - 1 - i] multiple times is annoying. We could simplify the code by using this trick:

<li *ngFor="let _ of users; index as i; trackBy:userIdentity">
<ng-container *ngIf="users[users.length - 1 - i] as user">
\{\{ user.name }} is \{\{ user.age }} years old.
</ng-container>
</li>

This way, we create a local variable which can be used inside the <ng-container>. Note that it will only work properly if your array elements are not falsy values because what we save in that variable is actually the condition for the *ngIf directive.

Also note that we have to use a custom TrackByFunction, and it is absolutely necessary if you don't want to spend a lot of time debugging afterwards, even though our simple example seems to work fine without it.

Live demo

2. Pure pipe that returns an iterable object

@Pipe({ name: 'reverseIterable' })
export class ReverseIterablePipe implements PipeTransform {
transform<T>(value: T[]): Iterable<T> {
return {
*[Symbol.iterator]() {
for (let i = value.length - 1; i >= 0; i--) {
yield value[i];
}
}
};
}
}

*ngFor loops work with iterable objects internally, so we just construct one that fulfills our needs using a pipe. The [Symbol.iterator] method of our object is a generator function which returns iterators that let us traverse the array in reverse order. Please read this guide if it is the first time you hear about iterables, or if the asterisk syntax and the yield keyword are unfamiliar to you.

The beautiful thing about this approach is that it is enough to just create one iterable object for our array. The app will still work properly if we make changes to the array because the generator function returns a new iterator each time it is called. That means that we only have to create a new iterable when we create a new array, and that is why a pure pipe is enough here, which makes this solution better than the one with slice() and reverse().

I consider this to be the more elegant solution than the previous one because we do not have to resort to using array indices in our *ngFor loop. In fact, we do not need to change anything in our component other than adding the pipe because the entire functionality is wrapped within it.

<li *ngFor="let user of users | reverseIterable">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>

Also, don't forget to import and declare the pipe in your module.

Note that I named the pipe differently this time in order to indicate that it only works in contexts where an iterable is enough. You cannot apply other pipes to its output if they expect arrays as their input, and you cannot use it to print the reversed array by writing \{\{ users | reverseIterable }} in your template like you can with normal arrays because the iterable object does not implement the toString() method (although nothing stops you from implementing it if it makes sense in your use case). That is the disadvantage this pipe has compared to the one with slice() and reverse(), but it is definitely better if all you want to do is to use it in *ngFor loops.

Live demo

Possible optimization with a stateful pure pipe

“Stateful pure pipes” may sound like an oxymoron, but they are actually something Angular lets us create if we use its relatively new compilation and rendering engine called Ivy.

Before Ivy was introduced, Angular would only create one instance of a pure pipe, even if we used it more than once in our template. That means that the transform() method would always be called on the same object. As a consequence, we could not rely on any state stored in that object because it was shared between all occurences of the pipe in our template.

Ivy, however, takes a different approach by creating a separate pipe instance for each of its occurrences, which means that we can now make pure pipes stateful. Why would we want to do that? Well, the problem with our current solution is that, although we do not have to create a new iterable object when we modfiy the array, we still have to do it when we create a new one. We could fix that by stroing the array in a property of our pipe instance and making the iterable depend on that property instead of the array passed to the transform() method. We then only need to assign the new array to our existing property whenever transform() is called, and just return the existing iterable object afterwards.

@Pipe({ name: 'reverseIterable' })
export class ReverseIterablePipe implements PipeTransform {
private array: any[] = [];
private reverseIterable: Iterable<any>;


constructor() {
this.reverseIterable = {
[Symbol.iterator]: function*(this: ReverseIterablePipe) {
for (let i = this.array.length - 1; i >= 0; i--) {
yield this.array[i];
}
}.bind(this)
};
}


transform<T>(value: T[]): Iterable<T> {
this.array = value;
return this.reverseIterable;
}
}

Note that we have to bind the generator function to this because this might have a different meaning in other contexts if we don't. We would normally fix that by using an arrow function, but there is no such syntax for generator functions, so we have to bind them explicitly.

Ivy is enabled by default starting with Angular version 9, but I disabled it in the live demo in order to show you why we cannot make pure pipes stateful without it. In order for the app to work correctly, you will have to set angularCompilerOptions.enableIvy to true in the tsconfig.json file.

Live demo

You can simply add slice() and reverse() like this:

<li *ngFor="let user of users.slice().reverse() ">
\{\{ user.name }} is \{\{ user.age }} years old.
</li>