$compile for Angular 4

Alright so, Angular 4 has a new compiling model (AoT) as well as JIT. Even though AoT is being enforced (not a bad thing at all, there are a lot of advantages to using it), if you want to do dynamic stuff as you used to in Angular 1 using $compile in a single line, easy as it should be, you can’t do so now due to their new paradigm.

I’ll be blunt and honest here: Frameworks should be easy to use, and when you improve it, you should try to keep developer usability. I mean, I can’t believe that the framework doesn’t have something that used to be so simple (and important!) in previous versions.

Some background: I was having an issue with Leaflet and asked for help in StackOverflow, so I’ll give credit where it is due: I got the main code from yurzui here, so I based my code off his solution to make a generic service out of this. Thanks a lot for sharing!

So, after around a week of trying to do it my own way, lo and behold, I present you my Custom Compile Service for Angular 4:

@Injectable()
class CustomCompileService {
  private appRef: ApplicationRef;
  constructor(
    private injector: Injector,
    private resolver: ComponentFactoryResolver
  ) { }
  configure(appRef) {
    this.appRef = appRef;
  }
  compile(component, onAttach) {
    const compFactory = this.resolver.resolveComponentFactory(component);
    let compRef = compFactory.create(this.injector);
    if (onAttach)
      onAttach(compRef);
    this.appRef.attachView(compRef.hostView);
    compRef.onDestroy(() => this.appRef.detachView(compRef.hostView));
    let div = document.createElement('div');
    div.appendChild(compRef.location.nativeElement);
    return div;
  }
}

How does it work?

Example repo here: https://github.com/darkguy2008/leaflet-angular4-issue

I’m not an Angular 4 expert… But from the top of my head, I think it uses a factory to resolve the component’s instance and build it. Then, it stores the component’s reference into compRef and then it attaches it to the appRef’s (ApplicationReference?) host view. Afterwards, the nativeElement is inserted into a div and returned to you.

There’s two caveats:

  1. appRef is an ApplicationRef and as such, it must come from a Component. I do have a repo where I use this service inside another service, so appRef is being passed from the first component that initializes the service with a custom init() method, and from the service it passes it to the CustomCompileService using the configure() method shown above.
  2. The components that you dynamically create must be declared in the entryComponents section of your main app’s NgModule or Component.

Example usage

  1. Declare your dynamic component (PopupComponent in this example):
@NgModule({ imports: [...], providers: [...], declarations: [ PopupComponent ], entryComponents: [ PopupComponent ], bootstrap: [AppComponent] })
class AppModule { }

2. Initialize the compile service and pass it an ApplicationRef. This comes from @angular/core and you can just pass it in the component’s constructor.

compileService.configure(this.appRef);

3. Compile your component and put its output somewhere. In my code, I used Leaflet’s setPopupContent:

this.compileService.compile(PopupComponent, (c) => { c.instance.param = 0; setInterval(() => c.instance.param++, 1000); })

What’s this? First parameter is the Component you want to compile. Second parameter is a function that allows you to apply binding to the component. For instance, I had {{ param }} in my component’s template, so, let’s say instance is the component’s this. I’m still not sure how to mix contexts but I think it works well this way. So, I set param to zero and added an interval to update the counter. As you can see in the repo when you run it, it will work as explained.

I hope this is useful for you! This is my first article on Medium, so I hope I did everything right and let me know your thoughts on this!.

-DARKGuy