WebCake

One of the challenges I noticed while working in Ionic 2 with a Firebase backend was that Angular 2 has a bit of a gripe with iterating over object properties by way of ngFor. At least at the time of this post, there’s nothing in the documentation on how to do it. I’ve seen a few posts on StackOverflow that have various recommendations, as well as issues logged in the Angular GitHub that ask for this to be implemented, with the Angular team shooting it down for what sound like logical reasons.

Still, this is something we as developers have to find a way around, so I thought I’d write up a post on how I was able to do it. Note that this follows the suggested approach found in the Angular issues, where a feature request was logged to bring back object property iteration.

Mapping Object keys

The first solution is to simply map object keys to a class property in the constructor function. Rather than iterating over properties in the object, the template for the ngFor will iterate over the keys, binding the repeated template to the current key in the array.

export class IterateOverObject {
    public arrayOfKeys;

    @Input dataObject;
    constructor() {
        this.arrayOfKeys = Object.keys(this.dataObject);
    }
}

Since that exposes a string value for us to use within the repeated template, we can use it as a key mapping for binding and displaying object data. Each item in the array allows us to map to its corresponding property in the dataObject.

<div *ngFor='#key of arrayOfKeys'>
    <h3>{{dataObject[key].someProperty}}</h3>
    <p>{{dataObject[key].anotherPropery}}</p>
</div>

Pretty straightforward, and very little overhead. The down side is that it’s not very portable. Implementing as a component means that it fits a specific use case or implementation, and would be a bit difficult to port over to other instances. Let’s fix that.

Implementing As a Pipe

You can extend this functionality to be a bit more modular by adding it as a Pipe in your application, and then import that class into whatever class is going to implement it.

Before we build it out, let’s make sure we cover some bases as far as acceptance criteria:

  • it should return an iterable that ngFor can understand, containing the data in a scoped object
  • developers should be ale to access the object’s property names as we iterate on their values
  • since objects don’t retain any order for their properties, it would be nice to be able to sort the iterable returned
  • it should be a class that can be implemented or extended

Based on that, we can get started. Let’s create our class with the basic building blocks first:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'values'})
export class ValuesPipe implements PipeTransform {
    transform(value: any, args?: any[]): any[] {
        // we'll put our functional code in here...
    }
}

At the outset, you can see that we’re importing the Pipe and PipeTransform components from Angular 2. The Pipe decorator will allow us to implement this class into other classes via their pipes property. PipeTransform is an interface that structures the functional programming and behavior of the pipe class. For instance, the arguments sent to the pre-set transform() method come pre-typed, as does the return value.

I added the optional aspect to the args argument by adding a question-mark. The thought there was that I wanted to allow for simple implementation with default behavior.

An array of individual values

The next step is to start writing the functionality that we have in the section above, where we map an object’s values into an array.

export class ValuesPipe implements PipeTransform {
    transform(value: any, args?: any[]): any[] {
        // create instance vars to store keys and final output
        let keyArr: any[] = Object.keys(value),
            dataArr = [];

        // loop through the object,
        // pushing values to the return array
        keyArr.forEach((key: any) => {
            dataArr.push(value[key]);
        });

        // return the resulting array
        return dataArr;
    }
}

Making the pipe available to our app

In order to use this pipe, we’d have to add it to the implementing modules’s declarations property in its @NgModule():

// in app.module.ts

// ...all the other imports
import {ValuesPipe} from 'path/to/pipe'

@NgModule({
    imports: [...],
    declarations: [
        // other components...
        ValuesPipe,
        // other pipes...
        // etc...
    ]
})

In our template, we’d add it using the pipe syntax that many of us are familiar with from Angular 1.x:

<div *ngFor='#item in dataObject | values'>...</div>

Maintaining the key

There are some use cases where maintaining the key string is going to be important. In my use case, when I was tackling this problem, I needed that key string to get settings values from a completely different object during my ngFor loop.

In that situation, you’re going to return an array of objects from your pipe (if you’re not already). Currently our code doesn’t care what data type want to return as an array, so you can keep the pipe slim. Let’s change that, using our args argument.

First, I’ll want to update the type of the return array:

export class ValuesPipe implements PipeTransform {
    transform(value: any, args?: any[]): Object[] {
    // ...

Changing the return data type to Object[] means that we’ll be sending back an array of objects.

In the next block, we’re going to use whatever value the user has set as the first argument to use as the property name for the key in the loop. It might be id, or key, or name or whatever. The goal here is to maintain that key string so that it can be used along with its value in the ngFor.

// create instance vars to store keys and final output
let keyArr: any[] = Object.keys(value),
    dataArr = [],
    keyProp = args[0] ? args[0] : 'key';

Now we have a property that the developer could have passed in to name the field that the key will be assigned to; or as a fallback, we’ll explicitly set it to key.

Next, we’ll want to build out our return object. For now I’ll just go with happy path, and you can extend the following block to fit your needs, as there are at least a dozen ways I can think of to customize the returned object.

// loop through the object,
// pushing values to the return array
keyArr.forEach((key: any) => {
    // create the literal from the key's value
    let retObj = {
        childValue: value[key]
    };
    // add the key as a property
    retObj[keyProperty] = key;
    dataArr.push(retObj);
});

Plenty of ways to flex this into a schema that suits your needs. In this case, there will be two top-level properties – a childValue, which will hold all of the contents of this particular property’s value, and a property containing the key, named by whatever argument was passed in the template. It’s really up to your use case to determine if that works for you.

Adding an argument to the pipe in the template is simple:

<div *ngFor='#item in dataObject | values:"keyName"'>...</div>

Alternately, if you already know that your values in the original data object are all objects themselves, you can use the first argument to simply set a new property on the value object:

keyArr.forEach((key: any) => {
    value[key][keyName] = key;
    dataArr.push(value[key])
});

In this case you’re adding the key as a property on its own value object. In doing that you can pass that value object to the array, with the key already set, and therefore accessible from within the ngFor.

Sorting the return array

Finally, we can add sorting to the array of keys so that our value objects are added to the return array in a particular order. In adding sorting to the keys, we’ll be able to set the order by which the pipe loops through them, and thus adds values to the array. In this case we actually gain some functionality over looping through object properties, as objects don’t maintain order.

This is actually something I needed in my implementation, so I thought I’d add it here. It’s also extremely simple.

// create instance vars to store keys and final output
let keyArr: any[] = Object.keys(value),
    dataArr = [],
    keyProp = args[0] ? args[0] : 'key';

if(args[1]) {
    keyArr.sort();
}

You would implement this by adding a second parameter to the template’s pipe:

<div *ngFor='#item in dataObject | values:"keyName":true'>...</div>

If set to true, the array will sort by whatever. Notice that I’m not putting the boolean in quotes. That will make sure it’s evaluated as a boolean instead of a string. Keep in mind that false is actually false, while the boolean value of 'false' is true.

Alternatively you might instead sort by the values of your return array. Instead of running the sort on the array of keys, you’ll sort the dataArr after all values have been added. In that case you might add an A/B sort function as your sort operator:

if(args[1]) {
    dataArr.sort((a: Object, b: Object): number => {
        return a[keyName] > b[keyName] ? 1 : -1;
    });
}

To keep things simple I’m just sorting by the array key again. But hopefully you get the point. You can sort by any value that you expect to find in that array of returned objects.

Conclusion

At the end of the day, this is what my pipe looks like:

import {Pipe, PipeTransform} from 'angular2/core';

@Pipe({name: 'values'})
export class ValuesPipe implements PipeTransform {
    transform(value: any, args?: any[]): Object[] {
        let keyArr: any[] = Object.keys(value),
            dataArr = [],
            keyName = args[0];

        keyArr.forEach((key: any) => {
            value[key][keyName] = key;
            dataArr.push(value[key])
        });

        if(args[1]) {
            dataArr.sort((a: Object, b: Object): number => {
                return a[keyName] > b[keyName] ? 1 : -1;
            });
        }

        return dataArr;
    }
}

You can find a working example on Plunker. And now it’s yours to do whatever you want with it. It’s certainly not built to fit all use cases, so you should modify or destroy it to suit your specific implementation.

Feel free to leave your thoughts in the comments below.

20 responses to “Looping Over Object Properties in Angular 2’s ngFor”

  1. Ganesh says:

    I am trying this pipe. I am getting the data from http request with ngOnInit. and trying the display in the view and am getting the error ‘Unexpected directive value ‘undefined’ on the View of component’ . Can you point towards a working example?

    • Colin says:

      Sure! Here’s a link: http://plnkr.co/edit/heyPxN?p=preview. In your case, though, it’s not so much the Pipe as it is that you haven’t declared NgFor as a directive in your component. Every component has to define the directives it’s going to use in some way. In Angular 2, you can declare those components individually, as I did in the Plunker, or you can import and set COMMON_DIRECTIVES, which includes the most common built-in directives you’re going to use. I’ve updated the Component block to show this as well. In Ionic 2 we get to be lazy, since the Page decorator automatically includes them.

  2. Spiros says:

    Great, I used this in my demo,with a little modification,returning an array of key-value pairs!
    Thanks,greetings from Athens,Greece.

  3. Uma Sankar says:

    Great!! Thanks!! It helps a lot.

    Keep posting these kind of articles.

  4. fre says:

    Thanks for this tutorial! I am getting an EXCEPTION: Error: Uncaught (in promise): Unexpected piped value ‘undefined’ on the View of component ‘DecisionListComponent’
    My json is a nested array of arrays. Sometimes the child (it’s a tags array) array has one or more objects. Maybe this causes issues?
    Thanks for any help!

    • Colin says:

      hmm, that might have something to do with it. Can you post either a plunker or a gist with a sample of your JSON schema so I can take a look at it? The pipe is specifically intended to be used with an object and its properties, so if you have a data model that doesn’t fit that schema it might not work. If that’s the case I’d be happy to help you modify/extend the pipe so that it works for you.

      • fre says:

        Hi Colin, very kind of you to help me with this! Sorry for not getting back earlier. I basically want to iterate over a 3-levels deep nested Json object (returned from a api request). It would show docs with their tags.

        Here’s the plunkr so far: https://plnkr.co/edit/p6rwjnTtD293fXuCL9sA?p=preview

        The error I get is : EXCEPTION: TypeError: can’t assign to properties of (new String(“202″)): not an object in [decision | values:”tags”:true in DecisionListComponent@32:12]

        • Colin says:

          Hey buddy, in this case you don’t really need the pipe, and can use NgFor as it comes right out of the box. NgFor is meant to iterate over elements in an array, and when it does it sets the element it finds to a local template variable (e.g. #d), and you can access that object’s properties from the template variable using dot-notation (e.g. d.doctags). I’ve created an updated plunker to show you what I mean: https://plnkr.co/edit/CwE5ddm1swYq1rfI6us1?p=preview.

          The pipe above is for when your data isn’t stored in an array but rather a single object, and you still need to loop over its properties. Here’s an idea of what that data might look like using your own example:

          this.docs = {
              202: {
                  doctags: [],
                  meetitem_title_pop: '...'
              },
              203: {
                  doctags: [],
                  meetitem_title_pop: '...'
              }
          }
          

          In this case you might want to loop over the two properties, while maintaining the key. That’s when you’d want to use this pipe:

          <div *ngFor='#d of docs | values="key"'>
              <h3>{{d.key}}: {{d.meetitem_title_pop}}</h3>
          </div>
          
  5. VN says:

    I am trying this pipe and it works fine. But I get an error in Terminal saying:
    error TS7006: Parameter ‘key’ implicitly has an ‘any’ type

    • Colin says:

      That’s a setting in the tsconfig.json that prevents anything in your app from remaining un-typed, thereby implying that it is of type any. I’ve updated the post and the plunker by explicitly setting the type. Alternatively if that’s something you’re not into, You can unset that by opening up tsconfig.json and looking for "noImplicitAny": false. If you’re using the Angular Quickstart project I think that was a recent update to have that set to true by default, and I’m personally inclined to think it’s a good plan.

  6. VN says:

    Colin,
    It worked!! Thanks for the quick reply and the update.You are the best 🙂

  7. Underwood says:

    Hey that’s nice thx for sharing!

    A quick question though. If I don’t want to sot with the items key for example but with the content.

    Let’s say that i have something like:

    this.myArray = [
    {
    id: 1,
    name: 'John'
    },
    {
    id: 2,
    name: 'Foo'
    },
    {
    id: 3,
    name: 'Bar'
    }
    ]

    How can I sort in this case with the name?

  8. Mike says:

    Hi Colin,
    Thanks so much for the article. I have a rather unique situation.
    I have 2 json arrays. One contains a watch list like:
    json1= [
    {“id”:6,”ticker”:”PG”,”gt_lt”:”<“,”price_trigger”:75},
    {“id”:8,”ticker”:”T”,”gt_lt”:”<“,”price_trigger”:39.5},
    {“id”:9,”ticker”:”WM”,”gt_lt”:”<“,”price_trigger”:60}
    ];

    The second one contains stock pricing info:
    json2 = [
    {“symbol”:”PG”,”name”:”Procter & Gamble Company (The) “,”lastTradeDate”:null,”lastTradePriceOnly”:88.4,”change”:0.35,”dividendYield”:3.04,”peRatio”:23.93,”volume”:1333524},
    {“symbol”:”T”,”name”:”AT&T Inc.”,”lastTradeDate”:null,”lastTradePriceOnly”:39.94,”change”:-0.26,”dividendYield”:4.78,”peRatio”:17.22,”volume”:2865061},
    {“symbol”:”WM”,”name”:”Waste Management, Inc. Common S”,”lastTradeDate”:null,”lastTradePriceOnly”:62.98,”change”:0.17,”dividendYield”:2.61,”peRatio”:24.61,”volume”:276554}
    ];

    I can get the first array’s data with ngFor with no problem. It’s the data from the second array I can’t access with ngFor. If I use nested NgFor’s I get a bunch of repeats. Would this be a good candidate for a Pipe?

    • Colin says:

      Hi Mike, it sounds like you’re trying to get the second array’s values based on what’s in the first list – is that right? So if a stock isn’t in my watch list, then its value in the second array won’t be displayed? I believe you could handle this with a Pipe by passing the watch list as a parameter to a Pipe that’s applied to the pricing info list, and then maintaining the comparison logic in the Pipe class. Just know that in that situation, you can only re-use that data in the markup. If you’re going to need that list in other places throughout your app, you might instead turn to a map-reduce workflow to send your data through – probably in a service – so that the reduced list of stock prices can be available throughout your app. I hope that answers your question, and let me know if you have any other thoughts.

  9. chapinme says:

    How can I get the corresponding result array of the filter? how pass these result array of filter to other variable?

Leave a Reply