Previously we tried hosting the newly created component in an HTML element, whether existing in body
, or created and appended to body
. Today we try two other ways:
- Hosting in
ng-template
- Content projecting in
ng-content
Follow along with StackBlitz project.
Hosting in a ng-template
What we need is a reference to that template, to use it as a host element. Is it doable? Let's dig in.
First, we add a template, and declare it. (I created a new fresh component in StackBlitz: DifferentComponent
).
// components/different.component
@Component({
template: `
//... add an ng-template where the new component shall appear
<ng-template #content></ng-template>
`
})
export class DifferentComponent {
// ...
// making it static, allows us to find it ahead of time, so that we can populate it
@ViewChild('content', {static: true, read: ElementRef}) content: ElementRef;
}
The simple way is to get the ElementRef
of the template, then find the nativeElement
(which is comment
node) then add element after it.
// different.component
// after creating a new component
const componentRef = createComponent(RosePartialComponent, {
environmentInjector: this.appRef.injector
});
this.appRef.attachView(componentRef.hostView);
// insert after
this.content.nativeElement
.after((<any>componentRef.hostView).rootNodes[0]);
Another way is using ng-container
instead of ng-template
. The result is identical.
Back to Rose dialog
Back to our Rose component, that is supposed to host Peach, programmatically. We won't stop appending Rose dialog itself in the body
, this is a clean solution. But we can make use of the above, by replacing the host element id="roseChild"
with a simple ng-template
.
// lib/RoseDialog/Rose.partial
@Component({
template: `
// remove this id: rosechild
<div class="modal-body" xid="rosechild" >
// and add a new template
<ng-template #content></ng-template>
</div>
`,
})
export class RosePartialComponent {
// ...
// make a reference to it
@ViewChild('content', { static: true, read: ElementRef })
content!: ElementRef;
//...
}
Then we can use it in our roseService
, we need to create the child component without setting its hostElement
property, then later insert it:
// lib/RoseDialog/rose.service
open(...) {
// ...
// ********* method 2: add to ng-template***********/
// first create child element
const childRef = createComponent(c, {
environmentInjector: this.appRef.injector,
});
// gain reference to content element, and child root
const nativeElement = componentRef.instance.content.nativeElement;
const childElement = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes[0];
// insert after
nativeElement.after(childElement);
//...
}
Using content projection
Finally, in our Rose dialog component, there is one more method to insert our new component (Peach) into the created dialog component (Rose). If we use ng-content
tag in Rose component, we can place anything in it using the projectableNodes
property in createComponent
. Confused? Here is how:
The optional parameter projectableNodes expects an array of an array of nodes: Node[][]
First, let's add the ng-content
tag into our Rose partial component
// lib/RoseDialog/rose.partial
@Component({
template: `
//... add an ng-content where the new peach component shall appear
<ng-content></ng-content>
`
})
Then the dialog service (roseService
) is supposed to create the child component first, before passing the root nodes to the projectableNodes
property. Note how you need to place the root nodes, which is an array, in another array for that to work: [rootNodes]
// lib/RoseDialog/rose.service
// create a new open method that uses the content projection
public openWithContent(...) {
// first, create the child component
const childRef = createComponent(c, {
environmentInjector: this.appRef.injector,
});
// attach view
this.appRef.attachView(childRef.hostView);
// get root nodes (array)
const rootNodes = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes;
// then create the dialog that will host it
const componentRef = createComponent(RosePartialComponent, {
environmentInjector: this.appRef.injector,
// pass the child nodes here (an array of an array)
projectableNodes: [rootNodes],
});
// append to body
this.doc.body.append(
(<EmbeddedViewRef<any>>componentRef.hostView).rootNodes[0]
);
// attach view
this.appRef.attachView(componentRef.hostView);
// passing properties and listening to events, is the same
// ...
return childRef.instance;
}
Projecting multiple slots programmatically
In my dialog service, I only need to pass one Peach into my Rose. But you might have different requirements, to pass multiple components. In the previous methods, using an exact HTML id, or using a defined ng-template
is straightforward, because you can gain direct reference to them. In the last method of projectable nodes, you can have multiple slots of ng-content
, and pass multiple elements to the projectableNodes
in the service, like this:
projectableNodes: [rootNodesOfA, rootNodesOfB]
You can also use querySelector
, to identify multiple slots in one Peach component. Using HTML
id and ng-template
is again easy, but in ng-content
, it isn't as clear as I wished it to be. Here is how.
Create a new component, and let's assume it has two regions: above and below.
// example Peach with multiple slots (banana.partial)
Component({
template: `
<div above>
Above part
</div>
<div below>
Below part
</div>
`,
// ...
})
export class BananaPartialComponent {}
In our Rose dialog, we can define multiple slots of ng-content
. Then in our dialog service (roseService
), we'll query for these parts and pass them in the projectableNodes
array:
// lib/RoseDialog/rose.service
public openWithContent(...) {
// ... create the childRef, then find the rootNode
// get root node
const rootNode = (<EmbeddedViewRef<any>>childRef.hostView).rootNodes[0];
// query select different parts
const above = rootNode.querySelector('[above]');
const below = rootNode.querySelector('[below]');
// then create the dialog that will host it
const componentRef = createComponent(RosePartialComponent, {
environmentInjector: this.appRef.injector,
// pass the children here
projectableNodes: [[above], [below]],
});
// then append to body and attach behavior as usual
// ...
return childRef.instance;
}
Unfortunately, the content will be projected in the order they appear in the projectableNodes
property. I tried using the select
attribute with ng-content
, but that made no difference.
Doesn't work: <ng-content select="[above]"></ng-content>
If you are interested in that path, you might dig deeper. As for me, I had enough of Bananas.
Providing instances to Peach
Our dialog component is almost ready, there is one small optional addition I want to learn about before jumping into house keeping and finalizing. What if we want to provide a local instance of a service to an inserted component? That and a rant about third party libraries is coming next. Stay tuned. 😴
Thank you for reading this far, did you get used to Rose and Peach already?
Top comments (0)