Iterate over object in Angular [duplicate]
Asked Answered
M

18

172

I am trying to do some things in Angular 2 Alpha 28, and am having an issue with dictionaries and ngFor.

I have an interface in TypeScript looking like this:

interface Dictionary {
    [index: string]: string
}

In JavaScript this will translate to an object that with data might look like this:

myDict={'key1':'value1','key2':'value2'}

I want to iterate over this and tried this:

<div *ngFor="(#key, #value) of myDict">{{key}}:{{value}}</div>

But to no avail, none of the below worked either:

<div *ngFor="#value of myDict">{{value}}</div>
<div *ngFor="#value of myDict #key=index">{{key}}:{{value}}</div>

In all cases I get errors like Unexpected token or Cannot find 'iterableDiff' pipe supporting object

What am I missing here? Is this not possible anymore? (The first syntax works in Angular 1.x) or is the syntax different for iterating over an object?

Marjana answered 18/7, 2015 at 11:30 Comment(5)
What is a "dictionary"? I've never seen or heard that term in a JavaScript, Angular, or TypeScript context. YBioplasm
Dictionary means a map I think, the term is not used at all in JS context but in Python or Ruby it does used.Madder
I think @bersling answer is now the correct answer to this question.Frisian
Please mark the correct answer better. bersling is correctPeggiepeggir
Take a look at this answer: https://mcmap.net/q/64751/-how-to-loop-over-object-properties-with-ngfor-in-angularGoda
U
94

It appears they do not want to support the syntax from ng1.

According to Miško Hevery (reference):

Maps have no orders in keys and hence they iteration is unpredictable. This was supported in ng1, but we think it was a mistake and will not be supported in NG2

The plan is to have a mapToIterable pipe

<div *ngFor"var item of map | mapToIterable">

So in order to iterate over your object you will need to use a "pipe". Currently there is no pipe implemented that does that.

As a workaround, here is a small example that iterates over the keys:

Component:

import {Component} from 'angular2/core';

@Component({
  selector: 'component',
  templateUrl: `
       <ul>
       <li *ngFor="#key of keys();">{{key}}:{{myDict[key]}}</li>
       </ul>
  `
})
export class Home {
  myDict : Dictionary;
  constructor() {
    this.myDict = {'key1':'value1','key2':'value2'};
  }

  keys() : Array<string> {
    return Object.keys(this.myDict);
  }
}

interface Dictionary {
    [ index: string ]: string
}
Urina answered 21/7, 2015 at 11:22 Comment(3)
i am trying same on object with key as number and value as string but angular is throwing error expression has changed after it was checked ? why so any idea ?Veraveracious
Yeah this is happening for me too. And same if I use @obsur's solution too.Probative
Please see bersling's answer because there is a trivial solution on the latest angular 7Peggiepeggir
C
258

Angular 6.1.0+ Answer

Use the built-in keyvalue-pipe like this:

<div *ngFor="let item of myObject | keyvalue">
    Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</div>

or like this:

<div *ngFor="let item of myObject | keyvalue:mySortingFunction">
    Key: <b>{{item.key}}</b> and Value: <b>{{item.value}}</b>
</div>

where mySortingFunction is in your .ts file, for example:

mySortingFunction = (a, b) => {
  return a.key > b.key ? -1 : 1;
}

Stackblitz: https://stackblitz.com/edit/angular-iterate-key-value

You won't need to register this in any module, since Angular pipes work out of the box in any template.

It also works for Javascript-Maps.

Cyanine answered 8/2, 2016 at 1:14 Comment(9)
You should add implements PipeTransform in the class definition (see angular.io/guide/pipes#custom-pipes)Brittain
@Brittain thanks, i've added it and updated to the new syntax of the for loop.Cyanine
Great answer, used this to ngFor my Dictionary. I had to do keyValuePair.val[0] though as my values ended up [{}] and not {}Unitive
Is there an advantage to this over just return Object.keys(dict).map(key => ({key, val: dict[key]}))?Buell
I don't see any, actually I'd use your way!Cyanine
note that you need to use at least angular 6.1.10 for the Angular 6+ answer, Tried it on 6.0.9 before and it did not work.Octane
@Octane Thanks for spotting this. I fixed the answer.Cyanine
When I see the error message in angular 7, there's no hint that this pipe exists. Could you please modify your answer to include "Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays." or something?Peggiepeggir
This worked for me! you just need to implement " keyvalue pipe " into *ngFor loop.Florella
U
94

It appears they do not want to support the syntax from ng1.

According to Miško Hevery (reference):

Maps have no orders in keys and hence they iteration is unpredictable. This was supported in ng1, but we think it was a mistake and will not be supported in NG2

The plan is to have a mapToIterable pipe

<div *ngFor"var item of map | mapToIterable">

So in order to iterate over your object you will need to use a "pipe". Currently there is no pipe implemented that does that.

As a workaround, here is a small example that iterates over the keys:

Component:

import {Component} from 'angular2/core';

@Component({
  selector: 'component',
  templateUrl: `
       <ul>
       <li *ngFor="#key of keys();">{{key}}:{{myDict[key]}}</li>
       </ul>
  `
})
export class Home {
  myDict : Dictionary;
  constructor() {
    this.myDict = {'key1':'value1','key2':'value2'};
  }

  keys() : Array<string> {
    return Object.keys(this.myDict);
  }
}

interface Dictionary {
    [ index: string ]: string
}
Urina answered 21/7, 2015 at 11:22 Comment(3)
i am trying same on object with key as number and value as string but angular is throwing error expression has changed after it was checked ? why so any idea ?Veraveracious
Yeah this is happening for me too. And same if I use @obsur's solution too.Probative
Please see bersling's answer because there is a trivial solution on the latest angular 7Peggiepeggir
E
72

try to use this pipe

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

@Pipe({ name: 'values',  pure: false })
export class ValuesPipe implements PipeTransform {
  transform(value: any, args: any[] = null): any {
    return Object.keys(value).map(key => value[key]);
  }
}

<div *ngFor="#value of object | values"> </div>
Eastsoutheast answered 3/12, 2015 at 19:26 Comment(6)
Brilliant, and if I want to keep the reference to the key I will just map an object with both key and value instead. I wish I could mark several answers as accepted answer, since this is the solution to my problem whilst the marked answer is the answer to my question.Marjana
@Eastsoutheast - If I do the above now, I get an error "expression has changed after it was checked" using angular2.beta.0.0. Any thoughts?Probative
Thats because pure: false requires a change detectionStrategy to be injectedHistochemistry
Why setting it to impure?Immobility
This worked well for me. Only thing was I could not use # in the ngFor. Used let instead.Disaffect
use object.values() insted of using own mapping example: return Object.values(value);Milissamilissent
M
35

Updated : Angular is now providing the pipe for looping through a json Object via keyvalue :

<div *ngFor="let item of myDict | keyvalue">
  {{item.key}}:{{item.value}}
</div>

WORKING DEMO , and for more detail Read


Previously (For Older Version) : Till now the best / shortest answer I found is ( Without any Pipe Filter or Custom function from Component Side )

Component side :

objectKeys = Object.keys;

Template side :

<div *ngFor='let key of objectKeys(jsonObj)'>
   Key: {{key}}

    <div *ngFor='let obj of jsonObj[key]'>
        {{ obj.title }}
        {{ obj.desc }}
    </div>

</div>

WORKING DEMO

Mcquoid answered 9/2, 2018 at 6:26 Comment(1)
let item of myDict | keyvalue this solved my problem.Sideburns
M
20

In addition to @obscur's answer, here is an example of how you can access both the key and value from the @View.

Pipe:

@Pipe({
   name: 'keyValueFilter'
})

export class keyValueFilterPipe {
    transform(value: any, args: any[] = null): any {

        return Object.keys(value).map(function(key) {
            let pair = {};
            let k = 'key';
            let v = 'value'


            pair[k] = key;
            pair[v] = value[key];

            return pair;
        });
    }

}

View:

<li *ngFor="let u of myObject | 
keyValueFilter">First Name: {{u.key}} <br> Last Name: {{u.value}}</li>

So if the object were to look like:

myObject = {
    Daario: Naharis,
    Victarion: Greyjoy,
    Quentyn: Ball
}

The generated outcome would be:

First name: Daario
Last Name: Naharis

First name: Victarion
Last Name: Greyjoy

First name: Quentyn
Last Name: Ball

Medrano answered 3/3, 2016 at 18:14 Comment(2)
just one thing to mention you need to change the View: as <li *ngFor="let u of myObject | keyValueFilter">First Name: {{u.key}} <br> Last Name: {{u.value}}</li>. +1 from me.Put
The code inside your map function could be simplified as: return { 'key' : key, 'value' : value[key] };Bynum
H
13

Adding to SimonHawesome's excellent answer. I've made an succinct version which utilizes some of the new typescript features. I realize that SimonHawesome's version is intentionally verbose as to explain the underlying details. I've also added an early-out check so that the pipe works for falsy values. E.g., if the map is null.

Note that using a iterator transform (as done here) can be more efficient since we do not need to allocate memory for a temporary array (as done in some of the other answers).

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

@Pipe({
    name: 'mapToIterable'
})
export class MapToIterable implements PipeTransform {
    transform(map: { [key: string]: any }, ...parameters: any[]) {
        if (!map)
            return undefined;
        return Object.keys(map)
            .map((key) => ({ 'key': key, 'value': map[key] }));
    }
}
Houghton answered 27/5, 2016 at 9:15 Comment(3)
love this thread, with one comment building ontop of another! I was about to write the same thing when I saw your codeMonteria
the only thing in this solution: it should implement PipeTransformGodfearing
@Godfearing Good point. I've updated my answer. I also return undefined instead of null.Houghton
P
11

Here's a variation on some of the above answers that supports multiple transforms (keyval, key, value):

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

type Args = 'keyval'|'key'|'value';

@Pipe({
  name: 'mapToIterable',
  pure: false
})
export class MapToIterablePipe implements PipeTransform {
  transform(obj: {}, arg: Args = 'keyval') {
    return arg === 'keyval' ?
        Object.keys(obj).map(key => ({key: key, value: obj[key]})) :
      arg === 'key' ?
        Object.keys(obj) :
      arg === 'value' ?
        Object.keys(obj).map(key => obj[key]) :
      null;
  }
}

Usage

map = {
    'a': 'aee',
    'b': 'bee',
    'c': 'see'
}

<div *ngFor="let o of map | mapToIterable">{{o.key}}: {{o.value}}</div>
  <div>a: aee</div>
  <div>b: bee</div>
  <div>c: see</div>

<div *ngFor="let o of map | mapToIterable:'keyval'">{{o.key}}: {{o.value}}</div>
  <div>a: aee</div>
  <div>b: bee</div>
  <div>c: see</div>

<div *ngFor="let k of map | mapToIterable:'key'">{{k}}</div>
  <div>a</div>
  <div>b</div>
  <div>c</div>

<div *ngFor="let v of map | mapToIterable:'value'">{{v}}</div>
  <div>aee</div>
  <div>bee</div>
  <div>see</div>
Particularly answered 11/1, 2017 at 22:32 Comment(1)
pure: false is really important for instant reflections.Ridge
D
4

I had a similar issue, built something for objects and Maps.

import { Pipe } from 'angular2/core.js';

/**
 * Map to Iteratble Pipe
 * 
 * It accepts Objects and [Maps](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
 * 
 * Example:
 * 
 *  <div *ngFor="#keyValuePair of someObject | mapToIterable">
 *    key {{keyValuePair.key}} and value {{keyValuePair.value}}
 *  </div>
 * 
 */
@Pipe({ name: 'mapToIterable' })
export class MapToIterable {
  transform(value) {
    let result = [];
    
    if(value.entries) {
      for (var [key, value] of value.entries()) {
        result.push({ key, value });
      }
    } else {
      for(let key in value) {
        result.push({ key, value: value[key] });
      }
    }

    return result;
  }
}
Dorsett answered 13/4, 2016 at 23:27 Comment(1)
This works good, except that in TypeScript you should add implements PipeTransform to the class definitionMonotony
S
3

Angular 2.x && Angular 4.x do not support this out of the box

You can use this two pipes to iterate either by key or by value.

Keys pipe:

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

@Pipe({
  name: 'keys',
  pure: false
})
export class KeysPipe implements PipeTransform {
  transform(value: any, args: any[] = null): any {
    return Object.keys(value)
  }
}

Values pipe:

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

@Pipe({
  name: 'values',
  pure: false
})
export class ValuesPipe implements PipeTransform {
  transform(value: any, args: any[] = null): any {
    return Object.keys(value).map(key => value[key])
  }
}

How to use:

let data = {key1: 'value1', key2: 'value2'}

<div *ngFor="let key of data | keys"></div>
<div *ngFor="let value of data | values"></div>
Shawm answered 30/5, 2017 at 6:15 Comment(0)
P
3
//Get solution for ng-repeat    
//Add variable and assign with Object.key

    export class TestComponent implements OnInit{
      objectKeys = Object.keys;
      obj: object = {
        "test": "value"
        "test1": "value1"
        }
    }
    //HTML
      <div *ngFor="let key of objectKeys(obj)">
        <div>
          <div class="content">{{key}}</div>
          <div class="content">{{obj[key]}}</div>
        </div>
Preussen answered 17/4, 2019 at 15:30 Comment(0)
M
2

If someone is wondering how to work with multidimensional object, here is the solution.

lets assume we have following object in service

getChallenges() {
    var objects = {};
    objects['0'] = { 
        title: 'Angular2', 
        description : "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
    };

    objects['1'] = { 
        title: 'AngularJS', 
        description : "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
    };

    objects['2'] = { 
        title: 'Bootstrap',
        description : "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
    };
    return objects;
}

in component add following function

challenges;

constructor(testService : TestService){
    this.challenges = testService.getChallenges();
}
keys() : Array<string> {
    return Object.keys(this.challenges);
}

finally in view do following

<div *ngFor="#key of keys();">
    <h4 class="heading">{{challenges[key].title}}</h4>
    <p class="description">{{challenges[key].description}}</p>
</div>
Moberg answered 8/1, 2017 at 20:32 Comment(0)
L
2

I have been tearing my hair out with trying to parse and use data returned form a JSON query/ api call. Im not sure exactly where i was going wrong, i feel like i have been circling the answer for days, chasing various error codes like:

"Cannot find 'iterableDiff' pipe supporting object"

"Generic TYpe Array requires one argument(s)"

JSON parsing Errors, and im sure others

Im assuming i just had the wrong combination of fixes.

So here's a bit of a summary of gotchas and things to look for.

Firstly check the result of your api calls, your results may be in the form of an object, an array, or an array of objects.

i wont go into it too much, suffice to say the OP's original Error of not being iterable is generally caused by you trying to iterate an object, not an Array.

Heres some of my debugging results showing variables of both arrays and objects

So as we generally would like to iterate over our JSON result we need to ensure it is in the form of an Array. I tried numerous examples, and perhaps knowing what i know now some of those would in fact work, but the approach i went with was indeed to implement a pipe and the code i used was that the posted by t.888

   transform(obj: {[key: string]: any}, arg: string) {
if (!obj)
        return undefined;

return arg === 'keyval' ?
    Object.keys(obj).map((key) => ({ 'key': key, 'value': obj[key] })) :
  arg === 'key' ?
    Object.keys(obj) :
  arg === 'value' ?
    Object.keys(obj).map(key => obj[key]) :
  null;

Honestly i think one of the things that was getting me was the lack of error handling, by adding the 'return undefined' call i believe we are now allowing for non expected data to be sent to the pipe, which obviously was occurring in my case.

if you don't want to deal with argument to the pipe (and look i don't think it's necessary in most cases) you can just return the following

       if (!obj)
          return undefined;
       return Object.keys(obj);

Some Notes on creating your pipe and page or component that uses that pipe

is i was receiving errors about ‘name_of_my_pipe’ not being found

Use the ‘ionic generate pipe’ command from the CLI to ensure the pipe modules.ts are created and referenced correctly. ensure you add the following to the mypage.module.ts page.

import { PipesModule } from ‘…/…/pipes/pipes.module’;

(not sure if this changes if you also have your own custom_module, you may also need to add it to the custommodule.module.ts)

if you used the 'ionic generate page' command to make your page, but decide to use that page as your main page, remember to remove the page reference from app.module.ts (here's another answer i posted dealing with that https://forum.ionicframework.com/t/solved-pipe-not-found-in-custom-component/95179/13?u=dreaser

In my searching for answers there where a number of ways to display the data in the html file, and i don't understand enough to explain the differences. You may find it better to use one over another in certain scenarios.

            <ion-item *ngFor="let myPost of posts">
                  <img src="https://somwhereOnTheInternet/{{myPost.ImageUrl}}"/>
                  <img src="https://somwhereOnTheInternet/{{posts[myPost].ImageUrl}}"/>
                  <img [src]="'https://somwhereOnTheInternet/' + myPost.ImageUrl" />
            </ion-item>

However what worked that allowed me to display both the value and the key was the following:

    <ion-list>  
      <ion-item *ngFor="let myPost of posts  | name_of_pip:'optional_Str_Varible'">

        <h2>Key Value = {{posts[myPost]}} 

        <h2>Key Name = {{myPost}} </h2>

      </ion-item>
   </ion-list>  

to make the API call it looks like you need to import HttpModule into app.module.ts

import { HttpModule } from '@angular/http';
 .
 .  
 imports: [
BrowserModule,
HttpModule,

and you need Http in the page you make the call from

import {Http} from '@angular/http';

When making the API call you seem to be able to get to the children data (the objects or arrays within the array) 2 different ways, either seem to work

either during the call

this.http.get('https://SomeWebsiteWithAPI').map(res => res.json().anyChildren.OrSubChildren).subscribe(
        myData => {

or when you assign the data to your local variable

posts: Array<String>;    
this.posts = myData['anyChildren'];

(not sure if that variable needs to be an Array String, but thats what i have it at now. It may work as a more generic variable)

And final note, it was not necessary to use the inbuilt JSON library however you may find these 2 calls handy for converting from an object to a string and vica versa

        var stringifiedData = JSON.stringify(this.movies);                  
        console.log("**mResults in Stringify");
        console.log(stringifiedData);

        var mResults = JSON.parse(<string>stringifiedData);
        console.log("**mResults in a JSON");
        console.log(mResults);

I hope this compilation of info helps someone out.

Longitudinal answered 7/1, 2018 at 7:24 Comment(0)
C
1

The dictionary is an object, not an array. I believe ng-repeat requires an array in Angular 2.

The simplest solution would be to create a pipe/filter that converts the object to an array on the fly. That said, you probably want to use an array as @basarat says.

Calaverite answered 19/7, 2015 at 12:59 Comment(0)
P
1

If you have es6-shim or your tsconfig.json target es6, you could use ES6 Map to make it.

var myDict = new Map();
myDict.set('key1','value1');
myDict.set('key2','value2');

<div *ngFor="let keyVal of myDict.entries()">
    key:{{keyVal[0]}}, val:{{keyVal[1]}}
</div>
Punt answered 21/9, 2016 at 4:47 Comment(0)
K
0

In JavaScript this will translate to an object that with data might look like this

Interfaces in TypeScript are a dev time construct (purely for tooling ... 0 runtime impact). You should write the same TypeScript as your JavaScript.

Kile answered 19/7, 2015 at 12:49 Comment(1)
that's not true, sry. type script forces you to write cleaner code. its much easier to abstract classes. which you simply don't have. C++ compiles to some asm - asm also has no classes or even types, nevertheless you write different c++ then ur asm code :PWonderwork
C
0

Define the MapValuesPipe and implement PipeTransform:

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

@Pipe({name: 'mapValuesPipe'})
export class MapValuesPipe implements PipeTransform {
    transform(value: any, args?: any[]): Object[] {
        let mArray: 
        value.forEach((key, val) => {
            mArray.push({
                mKey: key,
                mValue: val
            });
        });

        return mArray;
    }
}

Add your pipe in your pipes module. This is important if you need to use the same pipe in more than one components:

@NgModule({
  imports: [
    CommonModule
  ],
  exports: [
    ...
    MapValuesPipe
  ],
  declarations: [..., MapValuesPipe, ...]
})
export class PipesAggrModule {}

Then simply use the pipe in your html with *ngFor:

<tr *ngFor="let attribute of mMap | mapValuesPipe">

Remember, you will need to declare your PipesModule in the component where you want to use the pipe:

@NgModule({
  imports: [
    CommonModule,
    PipesAggrModule
  ],
...
}
export class MyModule {}
Clock answered 11/5, 2018 at 0:39 Comment(0)
L
0

So I was going to implement my own helper function, objLength(obj), which returns just Object(obj).keys.length. But then when I was adding it to my template *ngIf function, my IDE suggested objectKeys(). I tried it, and it worked. Following it to its declaration, it appears to be offered by lib.es5.d.ts, so there you go!

Here's how I implemented it (I have a custom object that uses server-side generated keys as an index for files I've uploaded):

        <div *ngIf="fileList !== undefined && objectKeys(fileList).length > 0">
          <h6>Attached Files</h6>
          <table cellpadding="0" cellspacing="0">
            <tr *ngFor="let file of fileList | keyvalue">
              <td><a href="#">{{file.value['fileName']}}</a></td>
              <td class="actions">
                <a title="Delete File" (click)="deleteAFile(file.key);">
                </a>
              </td>
            </tr>
          </table>
        </div>
Lethia answered 23/4, 2019 at 19:19 Comment(0)
T
0

There's another way to loop over objects, using structural directives:

I prefer this approach because it "feels" most like the normal ngFor loop. :-)

(In this case for example I added Angular's context variables let i = index | even | odd | first | last | count) that are accessible inside my loop).

@Directive({
  selector: '[ngForObj]'
})
export class NgForObjDirective implements OnChanges {

  @Input() ngForObjOf: { [key: string]: any };

  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.ngForObjOf && changes.ngForObjOf.currentValue) {
      // remove all views
      this.viewContainerRef.clear();

      // create a new view for each property
      const propertyNames = Object.keys(changes.ngForObjOf.currentValue);
      const count = propertyNames.length;

      propertyNames.forEach((key: string, index: number) => {
        const even = ((index % 2) === 0);
        const odd = !even;
        const first = (index === 0);
        const last = index === (count - 1);

        this.viewContainerRef.createEmbeddedView(this.templateRef, {
          $implicit: changes.ngForObjOf.currentValue[key],
          index,
          even,
          odd,
          count,
          first,
          last
        });
      });
    }
  }
}

Usage in your template:

<ng-container *ngForObj="let item of myObject; let i = index"> ... </ng-container>

And if you want to loop using an integer value, you can use this directive:

@Directive({
   selector: '[ngForInt]'
})
export class NgForToDirective implements OnChanges {

  @Input() ngForIntTo: number;
 
  constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) {

  }

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.ngForIntTo && changes.ngForIntTo.currentValue) {
      // remove all views
      this.viewContainerRef.clear();

      let currentValue = parseInt(changes.ngForIntTo.currentValue);
      for (let index = 0; index < currentValue; index++) {
        this.viewContainerRef.createEmbeddedView(this.templateRef, {
          $implicit: index,
          index
        });
      }

    }

  }
}

Usage in your template (example: loop from 0 to 14 (= 15 iterations):

<ng-container *ngForInt="let x to 15"> ... </ng-container>
Toolmaker answered 4/2, 2021 at 13:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.