Adventures in Angular Upgrading
This is a technical log of issues and observations I’ve made while upgrading a large AngularJS 1.6 application to Angular 4.*.
Thursday, 27th of July
Yesterday I wrote about injecting a parent component into an agGrid renderer component. I did this by giving the child an explicit dependency on the type of the parent. But what if you want to make the renderer more generic, and reusable across different agGrid-owners? In that case, we need to abstract the relationship somewhat.
In a language like C# or Java, the solution would be simple: Make the parent(s) implement an interface, then inject that interface rather than the concrete type. We can use the same approach here, but things are a little more complicated in TypeScript. Interface types are not reified, e.g. they have no runtime representation. Even with metadata turned on, you cannot reflect on a class and see what interfaces it implements.
Once again, the Angular team are ahead of us, and have included a solution: InjectionToken<T>. By declaring an instance of this class, we create a token that other classes can use in order to notify the DI system that they provide an implementation of a particular interface.
E.g. if I have an interface like this, that I want to be injectable:
export interface Clearable {
clearAll(): void;
}I need to declare a corresponding InjectionToken:
export const CLEARABLE = new InjectionToken<Clearable>('Clearable');The parent component should then use this token in a provider declaration, supplying itself as the injected value:
@Component({
providers: [{
provide: CLEARABLE,
useExisting: MyParent
}]
})
export class MyParent implements Clearable {
...
}This is the same pattern that Angular forms uses with CONTROL_VALUE_ACCESSOR to let custom controls support ngModel. One wrinkle is that, if your interface is declared in the same file as “real” classes, then you will probably get warnings from Webpack that it cannot be found. This comment on a corresponding angular-cli issue tries to describe the problem. Sufficed to say, it’s a result of Webpack and TypeScript not working perfectly together. The simplest solution—other than to just ignore the warning—is to move the interface declaration into a separate file.
The child component can then inject the parent component using the injection token:
@Component({ ... })
export class MyChild {
constructor(@Inject(CLEARABLE) private clearable: Clearable) {
...
}
}Note that this also breaks the circular dependency, so a forwardRef() is no longer necessary.
While playing around with the above, I experimented with dynamically creating an instance of MyChild within MyParent using ComponentFactoryResolver. My initial attempt looked like this:
class MyParent {
constructor(
cfr: ComponentFactoryResolver,
vcr: ViewContainerRef
) {
let factory = cfr.resolveComponentFactory(MyChild);
vcr.createComponent(factory);
}
}Yet, to my confusion, this resulted in an error, saying the MyParent dependency on MyChild could not be resolved. Why? Because of the way I was injecting ViewContainerRef. My—incorrect—understanding, was this this was a reference to MyParent's view. In fact, it is a reference to the parent container.
In order for this to work, I need to get a ViewContainerRef from a child of MyParent's view. The Angular documentation suggests create a new directive that exposes its own ViewContainerRef and adding it to the component’s view template. However, you can get around having to create a new directive by using the <template> element and ViewChild, as follows:
@Component({
template: `<template #insertionPoint></template>`,
providers: [ ... ]
})
export class MyParent implements AfterViewInit {
@ViewChild('insertionPoint', { read: ViewContainerRef })
insertionPoint: ViewContainerRef; constructor(
private cfr: ComponentFactoryResolver
) { } ngAfterViewInit() {
let factory = this.cfr.resolveComponent(MyChild);
this.insertionPoint.createComponent(factory);
}
}
Wednesday, 26th of July
Today I worked on getting a number of complex agGrid instances converted. This involved rewriting a number of custom AngularJS cell and header templates as components. This went relatively smoothly, as the interface to implement is pretty much the same, it’s just a different set of boilerplate to wrap around it that is different.
One interesting question was how to handle communication between the child components and the agGrid-owning parent component in the absence of a scope. Ideally the child would fire events, but agGrid doesn’t seem to support this. There are ways to supply custom parameters to component instances, so the parent could pass this to the child as a parameter. However, I figured I could use dependency injection for this instead, and have the child inject the parent as a constructor parameter, and then call methods on it.
At first, this didn’t seem to work. I kept getting a DI error saying the parent component dependency couldn’t be resolved, and when I debugged it, the DI token returned by reflection seemed to be undefined, rather than the parent class, as I expected. I wondered if it was something to do with how agGrid was instantiating the dynamic components, but it turned out I was being bitten by a circular dependency issue in TypeScript.
The problem is, if you have two classes that depend on each other, like this:
export class Triangle {
constructor(private square: Square) { }
}export class Circle {
constructor(private circle: Circle) { }
}
Then the emitted metadata for one of the classes’ constructor properties will be undefined.
Fortunately, the Angular team knew this was a potential issue, and included forwardRef() as a workaround. This lets you supply a function that tells Angular the type to be injected, rather than it relying on the class metadata. So the above would become:
export class Circle {
constructor(@Inject(forwardRef(() => Square)) private square: Square)
} I also looked into upgrading the modal dialog system. The current, AngularJS implementation is a fairly basic, somewhat hacky one that I cobbled together years ago based on a few examples around the web. One nice feature though is that it integrates with ui-router and lets you navigate to modal states.
For the Angular replacement, I’ve been looking at how Google’s Material project does things. In particular, it’s use of “portals” as generic places where you can insert dynamic content into the DOM. They’ve made this available as part of the Component Development Kit, so you can use it without taking a dependency on the whole of the Material library. Unfortunately, a lot of the most useful stuff, such as overlay and modal generation services, remains within the Material library, rather than the CDK. All you get is the very basic portal support. Hopefully they’ll migrate more of the generic services into the CDK eventually.
Ultimately, I decided to leave the modal system in AngularJS for now. It’ll be easier to convert once the rest of the application is finished, and I have a better understanding of how Angular’s dynamic component instantiation works.
Saturday, 22nd of July
It’s a Saturday, but it’s a rainy one, and I’m at a loose end, so I’m in work continuing with the Angular upgrade. It was a productive day, and I almost managed to complete the upgrade of a whole front-end module.
Mistakes I made today:
- Forgetting to re-export the agGrid module from the common module, and wondering why all my
<ag-grid-angular>instances weren’t recognised by the template compiler. - Forgetting to unregister
ui-routertransition hooks when a component is destroyed. It would be nice if TypeScript had some kind of support for marking functions that return a resource that needs disposing of. Perhaps with some kind of@@disposabledecorator?
