1

I am using the vmware/clarity design system and I'm trying to implement dynamic app-level alerts using the dynamic component loading outlined in angular.io. I've followed this pattern before, but I can't seem to get this to work with the alerts coming from Clarity.

app.component.ts

import { AfterViewInit, Component, ComponentFactoryResolver, OnDestroy, ViewChild } from '@angular/core';
import { ClrAlert } from '@clr/angular';

import { AlertsHostDirective } from './directives/alerts-host.directive';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit, OnDestroy {

  @ViewChild(AlertsHostDirective) alerts: AlertsHostDirective;
  private interval: NodeJS.Timer;

  constructor(private _componentFactoryResolver: ComponentFactoryResolver) { }

  ngAfterViewInit(): void {
    this.alert();
    this.getAlerts();
  }

  ngOnDestroy(): void {
    clearInterval(this.interval);
  }

  private alert() {
    const componentFactory = this._componentFactoryResolver.resolveComponentFactory(ClrAlert);

    const viewContainerRef = this.alerts.viewContainerRef;
    const componentRef = viewContainerRef.createComponent(componentFactory);

    componentRef.instance.isAppLevel = true;
    componentRef.changeDetectorRef.detectChanges();
  }

  private getAlerts() {
    this.interval = setInterval(() => {
      this.alert();
    }, 5000);
  }

}

app.component.html

<clr-main-container>
    <clr-alerts>
        <ng-template appAlertsHost></ng-template>
        <clr-alert clrAlertType="info"
                   [clrAlertAppLevel]="true">
            <div class="alert-item">
                <span class="alert-text">This is the first app level alert.    </span>
                <div class="alert-actions">
                    <button class="btn alert-action">Fix</button>
                </div>
            </div>
        </clr-alert>
        <clr-alert clrAlertType="danger"
                   [clrAlertAppLevel]="true">
            <div class="alert-item">
                <span class="alert-text">This is a second app level alert.</span>
                <div class="alert-actions">
                    <button class="btn alert-action">Fix</button>
                </div>
            </div>
        </clr-alert>
    </clr-alerts>
...

alerts-host.directive.ts

import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appAlertsHost]'
})
export class AlertsHostDirective {

  constructor(public viewContainerRef: ViewContainerRef) { }

}

If I put the directive on the ClrAlerts component, it works the way it should in that app level alerts are appended after the ClrAlerts in the DOM. But I want all my app-level alerts to appear inside that component as they come in. In turn, I'd like for the pager component that appears to be updated as well.

Is this possible?

Jake Smith
  • 2,332
  • 1
  • 30
  • 68
  • This answer can help you https://stackoverflow.com/questions/40922224/angular2-component-into-dynamicaly-created-element/40926110#40926110 – yurzui Feb 12 '18 at 05:59
  • 1
    You're literally always creating the `ClrAlert` component, how is that dynamic? In your example, you could just use a simple `*ngFor` and it would work fine. – Eudes Feb 13 '18 at 15:27
  • @Eudes , Maybe there is a better word to use than dynamic, but I'm borrowing the verbage from the angular.io documentation for the dynamic component loader. Using the pattern they outline, you are loading the same component, strongly typed, just as you say. What I think is meant by dynamic is "at run-time." In other words, while the application is already loaded by the client, components can be created based on user interaction or other events. `*ngFor` would require me to know all the notifications before the app is bootstrapped, which misses the point of what I'm trying to do. – Jake Smith Feb 13 '18 at 16:56
  • *ngFor is dynamic, you don't need to know all the notifications at runtime. If you have a global provider in your app that keeps track of all notifications that should be displayed, and user interactions and events update that list, then *ngFor would work perfectly for your use case. – Eudes Feb 13 '18 at 17:57
  • That's true. Let me give that a shot... – Jake Smith Feb 13 '18 at 18:18

2 Answers2

3

After the discussing in the comments, I figured it would be easier if I just posted what I meant by simply using *ngFor: https://stackblitz.com/edit/clarity-light-theme-v11-vs8aig?file=app%2Fnotifications.provider.ts

Using the component factory to create always the same component is completely overkill and is a terrible practice. The section of the Angular docs you linked specifically explains that their use case is to instantiate a new component dynamically when they don't even know the actual class of the component in the first place. I hope my example above will help you understand how to organize these alerts in your project.

Eudes
  • 1,521
  • 9
  • 17
  • 1
    This looks like it'll work. I was on my way to something similar that you have put together. However, I think I was again over-complicating and making the notifications an `Observable` rather than just a straight array I could push to. – Jake Smith Feb 13 '18 at 18:40
  • One thing I noticed is that if you start with notifications to begin with, remove them using the 'x' buttons, and then add more, things don't show up quite right. The first one you add doesn't show up at all, and the second one seems to make for itself, but it does not render well. This seems like a bug with Clarity? – Jake Smith Feb 13 '18 at 20:09
  • Yes, the notification isn't removed from the main array when the user clicks the "x". You can wire this just by listening to `(clrAlertClosedChange)`: https://stackblitz.com/edit/clarity-light-theme-v11-9l2ka3?file=app%2Fapp.component.html – Eudes Feb 13 '18 at 20:37
  • But with this approach, you still need to explicitly set the current index after adding an alert after removing all of the alerts. – Jake Smith Feb 13 '18 at 20:42
  • Which kinda sucks because I'm not sure how else to get access to the `MultiAlertService` to set the current index without keeping this code in the `AppComponent` to get the `ViewChild` – Jake Smith Feb 13 '18 at 20:47
  • Currently this is best solution as dynamic components are added as a siblings of viewContainerRef not child. Secondly how we can add the action especially when alert is added from child component using observables? – Samiullah Khan Jul 08 '18 at 06:36
1

@Eudes pointed out that I was making things too complicated. Basically, I just need to keep track of an array of notifications so that when something "alert-able" happens, I can just push an alert object onto the array and have them render through *ngFor.

However, there seems to an issue with the ClrAlerts parent component handling these changes correctly in certain situations. For example, if you start with alerts in the array, close them with user interaction, and then add more, the current alert is not set correctly. Therefore, this seems to be the cleanest solution I could come up with:

Template

<clr-main-container>
    <clr-alerts>
        <clr-alert *ngFor="let alert of alerts"
                   [clrAlertType]="alert.type"
                   [clrAlertAppLevel]="true">
            <div class="alert-item">
                <span class="alert-text">{{alert.text}}</span>
                <div class="alert-actions"
                     *ngIf="alert.action">
                    <button class="btn alert-action">{{alert.action}}</button>
                </div>
            </div>
        </clr-alert>
    </clr-alerts>
    ...

Component

import 'rxjs/add/Observable/of';

import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core';
import { ClrAlerts } from '@clr/angular';

export interface AppLevelAlert {
  type: 'info' | 'warning' | 'success' | 'danger';
  text: string;
  action: string;
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements AfterViewInit, OnDestroy {

  @ViewChild(ClrAlerts) alertsContainer: ClrAlerts;
  private interval: NodeJS.Timer;
  alerts: AppLevelAlert[] = [];

  ngAfterViewInit(): void {
    this.alert();
    this.getAlerts();
  }

  ngOnDestroy(): void {
    clearInterval(this.interval);
  }

  private alert() {
    this.alerts.push({
      type: 'success',
      text: 'This was added later!',
      action: 'Do nothing'
    });
    if (!this.alertsContainer.currentAlert) {
      this.alertsContainer.multiAlertService.current = 0;
    }
  }

  private getAlerts() {
    this.interval = setInterval(() => {
      this.alert();
    }, 5000);
  }

}
Jake Smith
  • 2,332
  • 1
  • 30
  • 68