Component open self in Dialog
Tags: angular, material
Published: 2020-05-14 21:41:25

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.

```html
<!-- 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>
```

```typescript
/* 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](https://stackoverflow.com/questions/53239164/send-data-to-a-templateref-matdialog) on StackOverflow. And it gave me an idea.

After some tinkering, I arrived at this result:

```html
<!-- 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`](https://angular.io/api/common/NgTemplateOutlet) structural attribute renders the template in question.
- `context` is a [microsyntax](https://angular.io/guide/structural-directives#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.


```typescript
/* 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.


```html
<!-- 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:


```typescript
/* 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.