Update: I wrote another blog post in which I explain how to achieve this exact same result using Portals from the Angular CDK. Please take a look at it. If you’re not into magic and you want to know how to do it from scratch, then keep reading this article.
If you’re somewhat like me, you just want to see the code and you’ll figure it out, so here you go. The following service has just one simple method appendComponentToBody
which will append your component as a direct child of the body and also add it to the Angular’s component tree (so that it can be included in the Angular’s change detection cycle). See working plunker at the end of this post.
If you got here after checking the code, then it means you need some explanation for what’s happening in that gist.
I first came across this problem a while ago when trying to add modals to my web app. I couldn’t find good documentation to dynamically add components to the body of my document. Some of the alternatives suggested adding helper components at the app.component.html
level to act as host containers, some others suggested passing the viewRef
in app.component.ts
to a custom service so that app-root
could be used later as an anchor, etc. None of them I liked, though. I then traveled to ng-conf looking for an answer and asked Jeremy Elbourn (@jelbourn) about this where he pointed me to a couple of utility classes inside Angular Material.
Simply put, the angular material repo is one hell of a good resource. You should be studying it on a daily basis if you truly want to understand how to build robust and well written Angular components: https://github.com/angular/material2.
Finding how they append components to the body took me a while to understand, though. They call a bunch of methods, from a bunch of files, but I’m here to break it down for you. Let’s jump right in.
Check the code snippet again and see the numbers in the comments. I’ll use those numbers to run the explanation.
Use the ComponentFactoryResolver
to create a reference of your component. As you’ll be adding your component to the AppRef
(see step 2), you need to create this component using the Angular’s default Injector which is provided in the constructor.
Please note that, at this point, you can use the componentRef
to bind data to your component’s inputs as componentRef.instance.myInput = 'yay!'
.
At this point, you have a reference to your component, but it doesn’t live in Angular’s world yet. The component is not inside the ng component tree so Angular won’t run change detection on it. To allow change detection on our component we should attach it to the ApplicationRef
.
We’re almost there. We have a componentRef
and it’s also living in Angular’s world. Now we want to see it rendered in the page and for that we have to get the DOM element out of the component. The code is pretty verbose to let TypeScript understand what we’re trying to achieve but, basically, we’re doing this to get the component’s DOM element: componentRef.hostView.rootnodes[0]
.
TL;DR. Use the good old document.body.appendChild
using the DOM element we got from step 3.
Some remarks about this. It’s been told that Angular is a platform and that no <body>
exists when running Angular on the server, on a Web Worker, on mobile (?), etc. But, having said that, I’m having a hard time trying to find a use case in which a dynamically created component will have to be rendered server-side, or some other context. I don’t know how it will work in Ionic, but for sure there’s a parent anchor DOM element which you can replace by body
to make this work.
This is let you know how to remove the component in just two steps:
ApplicationRef
so that no change detection will be performed by Angular.Very important: You will have to add your component to entryComponents
in your main module declaration. This will allow Angular to create your component on the fly.
Working plunker: https://plnkr.co/edit/Yc1ijM6shHt2JAPi7Fdg
Angular plugin using this approach: https://github.com/jdjuan/ng-notyf
Show some love if you liked it. Leave a comment. Reach out to me at Twitter @caroso1222.