180

I am new to angular 5 and trying to iterate the map containing another map in typescript. How to iterate below this kind of map in angular below is code for component:

import { Component, OnInit} from '@angular/core';

@Component({
  selector: 'app-map',
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.css']
})
export class MapComponent implements OnInit {
  map = new Map<String, Map<String,String>>();
  map1 = new Map<String, String>();

  constructor() { 


  }

  ngOnInit() {
    this.map1.set("sss","sss");
    this.map1.set("aaa","sss");
    this.map1.set("sass","sss");
    this.map1.set("xxx","sss");
    this.map1.set("ss","sss");


    this.map1.forEach((value: string, key: string) => {
      console.log(key, value);

    });


    this.map.set("yoyoy",this.map1);

  }



}

and its template html is :

<ul>
  <li *ngFor="let recipient of map.keys()">
    {{recipient}}
   </li>


</ul>

<div>{{map.size}}</div>

runtime error

Vivek Doshi
  • 56,649
  • 12
  • 110
  • 122
Feroz Siddiqui
  • 3,840
  • 6
  • 34
  • 69

6 Answers6

441

For Angular 6.1+ , you can use default pipe keyvalue ( Do review and upvote also ) :

<ul>
    <li *ngFor="let recipient of map | keyvalue">
        {{recipient.key}} --> {{recipient.value}}
    </li>
</ul>

WORKING DEMO


For the previous version :

One simple solution to this is convert map to array : Array.from

Component Side :

map = new Map<String, String>();

constructor(){
    this.map.set("sss","sss");
    this.map.set("aaa","sss");
    this.map.set("sass","sss");
    this.map.set("xxx","sss");
    this.map.set("ss","sss");
    this.map.forEach((value: string, key: string) => {
        console.log(key, value);
    });
}

getKeys(map){
    return Array.from(map.keys());
}

Template Side :

<ul>
  <li *ngFor="let recipient of getKeys(map)">
    {{recipient}}
   </li>
</ul>

WORKING DEMO

Vivek Doshi
  • 56,649
  • 12
  • 110
  • 122
  • 1
    if map = new Map>(); how does it works? – Feroz Siddiqui Jan 10 '18 at 12:48
  • @FerozSiddiqui, I have updated the answer , and I think that will work for any nested level. – Vivek Doshi Jan 10 '18 at 13:40
  • we lost all Map feature if we convert it into Array finally. what is the benefit of Map then? – xkeshav Oct 05 '18 at 04:58
  • 1
    @pro.mean , we are converting just because we want to loop through it via angular template as it want some iterable to iterate. and you still have original object `map` available you can use that one to use all benefit of Map – Vivek Doshi Oct 05 '18 at 05:06
  • I understand your point. but when we finally have to loop why we have used *Map*, simply can be done with the *Array*. I was looking for the iterator for Map. – xkeshav Oct 05 '18 at 05:09
  • 1
    @pro.mean , we have converted just to show that on template side , but you still have Map obaject that you can use in your component side. you can ask to OP `why he needed this kind of requirement` , he can explain you better :) – Vivek Doshi Oct 05 '18 at 05:12
  • @VivekDoshi Using angular6.1 method I had an issue where I lost the MAP ordering, so I had to user getKeys method to keep the ordering – Logan Wlv May 22 '19 at 17:10
  • Works also for [Record](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt) data type – liorko Nov 13 '19 at 12:16
  • keyvalue pipe works great, even though Intellij IDEA says that the argument is not assignable to this type. – Tomas Lukac Dec 25 '19 at 14:16
  • keyvalue pipe is removing the inserted order – Phaneendra Charyulu Kanduri May 16 '23 at 12:54
73

If you are using Angular 6.1 or later, the most convenient way is to use KeyValuePipe

   @Component({
      selector: 'keyvalue-pipe',
      template: `<span>
        <p>Object</p>
        <div *ngFor="let item of object | keyvalue">
          {{item.key}}:{{item.value}}
        </div>
        <p>Map</p>
        <div *ngFor="let item of map | keyvalue">
          {{item.key}}:{{item.value}}
        </div>
      </span>`
    })
    export class KeyValuePipeComponent {
      object: Record<number, string> = {2: 'foo', 1: 'bar'};
      map = new Map([[2, 'foo'], [1, 'bar']]);
    }
dota2pro
  • 7,220
  • 7
  • 44
  • 79
Londeren
  • 3,202
  • 25
  • 26
  • 3
    Hopefully people scroll and see this answer, because I was about to do a refactor until I had seen this out-of-the-box pipe available for my `map` type which takes care of using it with `*ngFor` – atconway Nov 01 '18 at 02:15
  • 3
    it seems, keyvalue does not keep initial sorting of map :-(, you need to add comparer function to fix that – Konstantin Vahrushev Feb 26 '20 at 18:16
  • 1
    This is true. And here is an issue https://github.com/angular/angular/issues/31420 – Londeren Feb 27 '20 at 13:13
  • Wow! I had no idea there was a pipe to iterate a map in the template so easily. This is so helpful. Thanks! – Meliovation Jul 09 '21 at 12:56
29

Edit

For angular 6.1 and newer, use the KeyValuePipe as suggested by Londeren.

For angular 6.0 and older

To make things easier, you can create a pipe.

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

@Pipe({name: 'getValues'})
export class GetValuesPipe implements PipeTransform {
    transform(map: Map<any, any>): any[] {
        let ret = [];

        map.forEach((val, key) => {
            ret.push({
                key: key,
                val: val
            });
        });

        return ret;
    }
}

 <li *ngFor="let recipient of map |getValues">

As it it pure, it will not be triggered on every change detection, but only if the reference to the map variable changes

Stackblitz demo

David
  • 33,444
  • 11
  • 80
  • 118
  • I think your pipe will be checked for changes and do it's full iteration on every change detection cycle as it isn't "pure". Didn't down vote but maybe that's why? – Drenai May 26 '18 at 09:43
  • 5
    @Ryan Pipes are pure by default. If you add a console.log in the pipe, you'll see that it does not get call multiple times. But component method in the accepted solution does... – David May 27 '18 at 06:20
  • 1
    I got that backward! Thanks for the heads up – Drenai May 27 '18 at 08:58
14

This is because map.keys() returns an iterator. *ngFor can work with iterators, but the map.keys() will be called on every change detection cycle, thus producing a new reference to the array, resulting in the error you see. By the way, this is not always an error as you would traditionally think of it; it may even not break any of your functionality, but suggests that you have a data model which seems to behave in an insane way - changing faster than the change detector checks its value.

If you do no want to convert the map to an array in your component, you may use the pipe suggested in the comments. There is no other workaround, as it seems.

P.S. This error will not be shown in the production mode, as it is more like a very strict warning, rather than an actual error, but still, this is not a good idea to leave it be.

Armen Vardanyan
  • 3,214
  • 1
  • 13
  • 34
5

Angular’s keyvalue pipe can be used, but unfortunately it sorts by key. Maps already have an order and it would be great to be able to keep it!

We can define out own pipe mapkeyvalue which preserves the order of items in the map:

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

// Holds a weak reference to its key (here a map), so if it is no longer referenced its value can be garbage collected.
const cache = new WeakMap<ReadonlyMap<any, any>, Array<{ key: any; value: any }>>();

@Pipe({ name: 'mapkeyvalue', pure: true })
export class MapKeyValuePipe implements PipeTransform {
  transform<K, V>(input: ReadonlyMap<K, V>): Iterable<{ key: K; value: V }> {
    const existing = cache.get(input);
    if (existing !== undefined) {
      return existing;
    }

    const iterable = Array.from(input, ([key, value]) => ({ key, value }));
    cache.set(input, iterable);
    return iterable;
  }
}

It can be used like so:

<mat-select>
  <mat-option *ngFor="let choice of choicesMap | mapkeyvalue" [value]="choice.key">
    {{ choice.value }}
  </mat-option>
</mat-select>
Patrick Smith
  • 518
  • 6
  • 9
2

As people have mentioned in the comments keyvalue pipe does not retain the order of insertion (which is the primary purpose of Map).

Anyhow, looks like if you have a Map object and want to preserve the order, the cleanest way to do so is entries() function:

<ul>
    <li *ngFor="let item of map.entries()">
        <span>key: {{item[0]}}</span>
        <span>value: {{item[1]}}</span>
    </li>
</ul>
Wildhammer
  • 2,017
  • 1
  • 27
  • 33