I wanted to build a single component which I can show in two completely different environments inside the same app.
- Inside a side-bar on a page (in-page).
- In a dialog outside the page (in-dialog).
The component should also have some action buttons attached to it.
- An expand button, which should only be available in-page.
- A collapse button, which should only be available in-dialog.
Things got pretty complicated in my mind so I decided to start simple - just build the in-page component. Once that's all done and working, I can research further.
<!-- app-description.component.html -->
<div class="wrapper">
<div class="description">
{{ description }}
</div>
<div class="footer">
<button mat-icon-button><mat-icon>fullscreen</mat-icon></button>
</div>
</div>
/* app-description.component.ts */
import { Component, Input, ChangeDetectionStrategy} from '@angular/core';
@Component({
selector: 'app-description',
templateUrl: './app-description.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DescriptionComponent {
@Input() description: string = '';
}
Nothing fancy. Now to the real question. How can I reuse my code and not create the same template twice, which would force me to maintain two templates?
My rule of thumb is that in 99.9% of the cases (that's 999 out of 1000), a question you have has already been asked and probably answered. The harder question is - can you find the aforementioned question and answer?
For this case, I came close. I found this post - Send data to a TemplateRef MatDialog on StackOverflow. And it gave me an idea.
After some tinkering, I arrived at this result:
<!-- app-description.component.html -->
<ng-container *ngTemplateOutlet="wrapper; context: { $implicit: description }"></ng-container>
<ng-tempalte #wrapper let-data>
<div class="wrapper">
<div class="description">
{{ data }}
</div>
<div class="footer">
<button
(click)="openDialog()"
mat-icon-button>
<mat-icon>fullscreen</mat-icon>
</button>
</div>
</div>
</ng-template>
OK, let's talk about what's going on here.
-
ng-container
is quite the Swiss army knife. It can take any template and plug it in where you want it. It is very useful when you want to build markup like you build code - keep the main short and descriptive, and have the details in sub sections. -
*ngTemplateOutlet
structural attribute renders the template in question. -
context
is a microsyntax ofngTemplateOutlet
which lets you define properties for the scope of the template. -
$implicit
is the template's way of applying a value to property without specifying it directly by name. -
let-data
defines a local propertydata
in the template's scope. We could have named it anything, but because we're going to use this same template for a dialog, this name specifically comes in very handy, as it's the property with which data is injected into a template.
/* app-description.component.ts */
import { Component, Input, ChangeDetectionStrategy, ViewChild, TemplateRef} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'app-description',
templateUrl: './app-description.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DescriptionComponent {
@ViewChild('wrapper') template: TemplateRef<any>;
@Input() description: string = '';
constructor(private dialog: MatDialog) {}
openDialog() {
this.dialog.open(this.template, { data: this.description });
}
}
Now, on the code side of things, we're using ViewChild
to get a hold of the same template we created for the ng-container
and feeding it into the dialog when it opens.
The last part I wanted to add in was to toggle between expand and collapse buttons, depending on the state of the component.
<!-- app-description.component.html -->
<ng-container *ngTemplateOutlet="wrapper; context: { $implicit: description }"></ng-container>
<ng-tempalte #wrapper let-data>
<div class="wrapper">
<div class="description">
{{ data }}
</div>
<div class="footer">
<button
*ngIf="!isDialogOpen"
(click)="openDialog()"
mat-icon-button>
<mat-icon>fullscreen</mat-icon>
</button>
<button
*ngIf="isDialogOpen"
(click)="openDialog()"
mat-icon-button>
<mat-icon>fullscreen_exit</mat-icon>
</button>
</div>
</div>
</ng-template>
So now we have two buttons, one appears when isDialogOpen
is true
and the other when false
. Here is the code for it:
/* app-description.component.ts */
import { Component, Input, ChangeDetectionStrategy, ViewChild, TemplateRef} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
@Component({
selector: 'app-description',
templateUrl: './app-description.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DescriptionComponent {
@ViewChild('wrapper') template: TemplateRef<any>;
@Input() description: string = '';
// Used to hide the "Open Dialog" button when component is loaded inside the dialog
public isDialogOpen: boolean = false;
constructor(private dialog: MatDialog) {}
openDialog() {
const dialogRef = this.dialog.open(this.template, { data: this.description });
dialogRef.afterOpened().subscribe(() => (this.isDialogOpen = true));
dialogRef.afterClosed().subscribe(() => (this.isDialogOpen = false));
}
}
Honestly, I'm not completely sure how come this works, as I wouldn't expect the dialog instance to have the same component scope as the in-page component.
Even more so, I expected that the in-page component would react to the change of the boolean and hide the button as well (which I didn't mind too much) - but it didn't! It stayed where it was, while the dialog component had the collapse button.
I hope to research the reasons for this in the future (or, discover the hard way that something isn't working as expected). In the meantime, I seem to have satisfied my requirements.
Top comments (0)