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.
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!