DEV Community

Cover image for Component open self in Dialog
Ayelet Dahan
Ayelet Dahan

Posted on

Component open self in Dialog

I wanted to build a single component which I can show in two completely different environments inside the same app.

  1. Inside a side-bar on a page (in-page).
  2. In a dialog outside the page (in-dialog).

The component should also have some action buttons attached to it.

  1. An expand button, which should only be available in-page.
  2. 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>
Enter fullscreen mode Exit fullscreen mode
/* 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 = '';

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 of ngTemplateOutlet 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 property data 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 });
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Latest comments (0)