U gebruikt een verouderde browser. Upgrade uw browser om deze site correct weer te geven.

Yielding to ngFor

Generators are Dead. Long Live Generators!

On the train home the other day I was listening to JavaScript Jabber episode 590. Somewhere during the episode, co-host Dan Shappir lamented the fact that generators in JS are dead - nobody uses them!

At the office that same day I had been teaching Angular to a colleague, and we discussed the following common situation: We have some data we want to display, say a Customer. There are a lot of properties we want to display. We want to be able to control both the order in which these items are shown, and add a user-readable label which may differ from the key in our model.

The straightforward implementation requires no abstraction, and simply copy pastes some boiler plate HTML for every item.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component({
  // ...configuration omitted for brevity
  template: `
    <mat-list>
      <mat-list-item>
        <span>Name</span>
        <span>{{ customer.name }}</span>
      </mat-list-item>
      <mat-list-item>
        <span>Street</span>
        <span>{{ customer.street }}</span>
      </mat-list-item>
      <mat-list-item>
        <span>City</span>
        <span>{{ customer.city }}</span>
      </mat-list-item>

      <!-- ...more items -->
    </mat-list>
  `,
})
export class DisplayCustomerDetailsComponent {
  @Input() customer: Customer
}

In many cases, this is *fine*. Put your code up for review and move on to the next ticket.

Except... What if we want to change the HTML, add a style class to each item or change the order in which they are displayed? We have to edit the html for each item individually. What if we want to hide certain fields depending on user roles? Or apply some other common logic? It's okay when we have 10 or maybe 20. But what about 50? 100?

If you have any experience with Angular you'll probably be shouting at me to use an *ngFor. I concur. But what do we iterate over? We could use the customer data, and use the keyvalue pipe to unpack it as an iterable. But that would prevent us from setting the order in which the items are shown.

So we can come up with the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Component({
  // ...configuration still omitted for brevity
  template: `
    <mat-list>
      <mat-list-item *ngFor="let key of keys">
        <span>{{ key }}</span>
        <span>{{ customer[key] }}</span>
      </mat-list-item>
    </mat-list>
  `,
})
export class DisplayCustomerDetailsComponent {
  @Input() customer: Customer

  protected keys = [
    'name', 'street', 'city', 'houseNumber', 'zipCode', // ...etc
  ]
}

Our ngFor iterates over a list of keys. This works! Except now we've lost control over the label. We could solve this by adding a labels property to the component with a mapping for key -> label. But then if we have any more additions like this (or changes to the model) it becomes tedious to maintain separate maps of keys.

So let's look for another solution instead. And to make Dan happy, let's use a generator while we're at it! Our 'field generator' will accept the customer data, an ordered list of keys and a configuration object, and return a Field for each key.

fieldGenerator
Not that kind of field generator.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Field {
  key: string
  label: string,
  value: unknown
}

type FieldConfig<T> = {
  [K in keyof T]?: { label?: string }
}

function* fieldGenerator<T>(
  data: T,
  keys: (keyof T)[],
  config: FieldConfig<T>
) {
  for (const key of keys) {
    yield {
      key: key as string,
      label: config[key]?.label || key as string,
      value: data[key],
    }
  }
}

Aside from some voodoo to add proper typing, the fieldGenerator() is very simple. It iterates over the list of keys in order and returns a Field object for each, informed by both the data and the configuration object.

Our component now looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component({
  // ...comment that configuration is omitted for brevity omitted for brevity
  template: `
    <mat-list>
      <mat-list-item *ngFor="let field of fields">
        <span>{{ field.label }}</span>
        <span>{{ field.value }}</span>
      </mat-list-item>
    </mat-list>
  `,
})
export class DisplayCustomerDetailsComponent {
  @Input({ required: true })
  set customer(value: Customer) {
    this.fields = fieldGenerator<Customer>(value, this.keys, this.config)
  }

  private keys = [
    'name', 'street', 'city', 'houseNumber', 'zipCode', // ...etc
  ]

  private config: FieldConfig<Customer> = {
    houseNumber: { label: 'Number' },
    zipCode: { label: 'Zip code' },
  }

  protected fields!: Iterable<Field>
}

What I like about this setup is that the config is extensible. The problem with iterating over a set of items is that customisation becomes hard. What if a particular field is not a string value but a nested object? What if we need to add a particular style class, or show an icon? Suddenly the iterative approach doesn't work and you either have to break out certain fields from the set, or go back to writing them all out manually. But with an extensible configuration we could do all these things; want to add a display function to certain fields, some style classes, or some other flag? Go ham. Add it to the config. Your code and html will stay simple and compact.

To Dan's point, I've used constructions like these before but it never occurred to me to use a generator function. If we take a list of keys, we can just as easily map over it to create a list of Field objects. Technically speaking, however, that does hurt the cpu faeries, as they will have to loop over the array twice; once in our map function, and then once more in the ngFor. By using a generator, we skip the initial loop without sacrificing readability of our code. So that is a pretty neat little improvement. Generators are dead. Long live generators!

Useful links:

The JavaScript Jabber episode, JSJ590

MDN page on Generators


We gebruiken cookies en vergelijkbare technologieën om de gebruikerservaring te verbeteren en om content te leveren die relevant is voor onze doelgroepen. Afhankelijk van hun doel, kunnen analyse- en marketingcookies worden gebruikt naast technisch noodzakelijke cookies. Door op "Akkoord" te klikken, verklaart u akkoord te gaan met het gebruik van bovengenoemde cookies.