I'm trying to implement a simple loading icon directive:
import { Directive, ElementRef, Renderer2, Input, OnChanges } from '#angular/core';
#Directive({
selector: '[appLoading]',
})
export class LoadingDirective implements OnChanges {
#Input() appLoading: boolean;
constructor(private elRef: ElementRef, private renderer: Renderer2) {
}
ngOnChanges() {
if (this.appLoading) {
this.renderer.addClass(this.elRef.nativeElement, 'loading-wrap');
this.elRef.nativeElement.innerHtml = '<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>';
} else {
this.renderer.setStyle(this.elRef.nativeElement, 'display', 'none');
}
}
}
In my template I have:
<div [appLoading]="isLoading"></div>
But it looks like Angular doesn't apply my styles for loading spinner in styles.css for the content added into innerHtml. The styles work for the main element with class 'loading-wrap' though.
The error was caused by innerHtml, it obviously should be innerHTML
this.elRef.nativeElement.innerHTML = '<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>';
I've got a structural directive that creates an embedded view by looking up a template ref using ng-template. My problem is that from this parent component (with structural directive), I cannot pass down children.
Parent component with structural directive
import { ViewChild, Component, OnInit, ElementRef } from "#angular/core";
import { TestJsonService } from "../../services/test-json.service";
#Component({
selector: "xfr-json-renderer",
template: `
<template-lookup></template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
<div>Why can't i pass this down to the child?</div>
</div>
`,
styleUrls: ["./json-renderer.component.css"],
})
export class JsonRendererComponent implements OnInit {
#ViewChild("childTemplate") childTemplate;
constructor(el: ElementRef, json: TestJsonService) {}
ngOnInit(): void {}
ngAfterViewInit() {}
}
Child component
import { Injectable, TemplateRef, Component, ViewChild } from "#angular/core";
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow></ng-template>
<ng-template #flexCol><xfr-flex-col>
// I want to pass the children into here
</xfr-flex-col></ng-template>
`,
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.templates.set("flexRow", this.flexRowTemplate);
this.service.templates.set("flexCol", this.flexColTemplate);
}
}
Structural directive
import { ViewContainerRef } from "#angular/core";
import { TemplateStore } from "./../services/composite-template.service";
import { Directive, Input } from "#angular/core";
#Directive({
selector: "[replaceWith]",
})
export class CompositeTemplateDirective {
#Input() replaceWith: "flex-col" | "flex-row";
constructor(private service: TemplateStore, private view: ViewContainerRef) {}
ngAfterViewInit() {
this.view.createEmbeddedView(this.service.templates.get(this.replaceWith));
}
}
The problem is that you need to use internal API for that, what is not the best thing. I would use it until I stay with the same angular version and would test it before every update - then should work stable.
I was able to do the injection with Angular 9, quite sure a similar solution (but different internal API) can be applied for other angular versions.
The main thing for the injection - where to inject the content, in components we could use ng-content, but here it wouldn't work, because we have different component contexts. In this case we could use <ng-template [ngTemplateOutlet]></ng-template> to tell the script where we want the injection.
here you can find a live demo: https://codesandbox.io/s/nifty-wright-335bm?file=/src/app/json-renderer.component.ts
CompositeTemplateDirective
import {NgTemplateOutlet} from '#angular/common';
import {AfterViewInit, Directive, Input, TemplateRef, ViewContainerRef} from '#angular/core';
import {TemplateStore} from 'src/app/TemplateLookup/TemplateLookup';
#Directive({
selector: '[replaceWith]',
})
export class CompositeTemplateDirective implements AfterViewInit {
#Input() replaceWith: 'flex-col' | 'flex-row';
constructor(
private service: TemplateStore,
private view: ViewContainerRef,
private templateRef: TemplateRef<any>,
) {
}
public ngAfterViewInit(): void {
const wrapper = this.service.templates.get(this.replaceWith);
const source = this.templateRef;
const view: any = this.view.createEmbeddedView(wrapper);
let directive: NgTemplateOutlet;
const nodes: Array<any> = view._lView ? view._lView : view._view && view._view.nodes ? view._view.nodes : [];
for (const node of nodes) {
if (typeof node !== 'object') {
continue;
}
if (node instanceof NgTemplateOutlet) {
directive = node;
}
if (typeof node.instance === 'object' && node.instance instanceof NgTemplateOutlet) {
directive = node.instance;
}
}
if (directive) {
directive.ngTemplateOutlet = source;
directive.ngOnChanges({
ngTemplateOutlet: {
previousValue: null,
currentValue: source,
firstChange: true,
isFirstChange: () => true,
},
});
}
}
}
TemplateLookup
import {AfterViewInit, Component, Injectable, TemplateRef, ViewChild} from '#angular/core';
#Injectable()
export class TemplateStore {
templates = new Map<string, TemplateRef<any>>();
}
#Component({
selector: 'template-lookup',
template: `
<ng-template #flexRow>
<div>
flexRow template
</div>
</ng-template>
<ng-template #flexCol>
<div>
<div>wrap</div>
<ng-template [ngTemplateOutlet]></ng-template>
<div>wrap</div>
</div>
</ng-template>
`,
})
export class TemplateLookup implements AfterViewInit {
#ViewChild('flexRow', {static: false}) flexRowTemplate;
#ViewChild('flexCol', {static: false}) flexColTemplate;
constructor(
private service: TemplateStore,
) {
}
ngAfterViewInit() {
console.log('TemplateLookup:ngAfterViewInit');
this.service.templates.set('flexRow', this.flexRowTemplate);
this.service.templates.set('flexCol', this.flexColTemplate);
}
}
so the most pragmatic thing here seems to be to just put the child you want to pass as a child of the template-lookup component and use ng-content...
do this in the parent:
<template-lookup>
<div>I will pass to child</div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
and this in the child:
<ng-template #flexRow></ng-template>
<ng-template #flexCol>
<xfr-flex-col>
<ng-content></ng-content>
</xfr-flex-col>
</ng-template>
and that will solve your problem / fulfill the stated requirements.
You could also consider a rewrite to your service to solve timing problems between templates being set and gotten once and for all:
import { Injectable, TemplateRef } from "#angular/core";
import {ReplaySubject} from 'rxjs';
import {map, filter, distinctUntilChanged} from 'rxjs/operators';
#Injectable({providedIn: 'root'}) // provide appropriately, root for example
export class TemplateStore {
private templates = new Map<string, TemplateRef<any>>();
private tmpSource = new ReplaySubject<Map<string, TemplateRef<any>>>(1);
setTemplate(key: string, template: TemplateRef<any>) {
this.templates.set(key, template);
this.tmpSource.next(this.templates)
}
getTemplate(key: string) {
return this.tmpSource.pipe(
map(tmpMap => tmpMap.get(key)),
filter(tmp => !!tmp),
distinctUntilChanged()
)
}
}
and make the associated changes in the directive and child components...
export class CompositeTemplateDirective implements OnInit, OnDestroy {
#Input() replaceWith: "flex-col" | "flex-row";
private sub: Subscription;
constructor(private service: TemplateStore, private viewContainer: ViewContainerRef) { }
ngOnInit() {
this.sub = this.service.getTemplate(this.replaceWith).subscribe(t => {
this.viewContainer.clear()
this.viewContainer.createEmbeddedView(t)
})
}
ngOnDestroy() {
this.sub.unsubscribe()
}
}
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
functioning example: https://stackblitz.com/edit/angular-ygdveu
it's been pointed out that this doesn't support nesting... so make the following adjustments and you can nest. in template lookup, you'll need to use the SkipSelf modifier in your constructor, and also provide the TemplateStore... in the case of no nesting, this will have no effect, SkipSelf just tells the injector to start looking for the service at the parent rather than at the component:
#Component({
selector: "template-lookup",
template: `
<ng-template #flexRow>FLEX ROW</ng-template>
<ng-template #flexCol>
FLEX COL
<div class="flex-col">
<ng-content></ng-content>
</div>
</ng-template>
`,
providers: [TemplateStore]
})
export class TemplateLookup {
#ViewChild("flexRow") flexRowTemplate;
#ViewChild("flexCol") flexColTemplate;
constructor(#SkipSelf() private service: TemplateStore) {}
ngAfterViewInit() {
this.service.setTemplate("flexRow", this.flexRowTemplate);
this.service.setTemplate("flexCol", this.flexColTemplate);
}
}
then you can nest to your hearts content like so:
<template-lookup>
<div>I can pass this to the child!</div>
<template-lookup>NESTED</template-lookup>
<div class="nested-content" *replaceWith="'flexCol'"></div>
</template-lookup>
<div class="NA-TEMPLATE-CHOOSER" *replaceWith="'flexCol'">
</div>
which is a little ugly, as you need to repeat the template-lookup component, but it does get the job done. This works by allowing the directive and template lookup to communicate with a different copy of the TemplateStore so you can nest different content.
working example of this variant: https://stackblitz.com/edit/angular-lpner2
I'm trying to automatically close an NG Bootstrap alert after a set period of time. The alert already has the close event which I'm using in the component. I'm adding the additional timeout functionality as a directive which should be able to trigger the close event itself. Something like this?
close-on-timeout.directive.ts
import { Directive, ElementRef, HostBinding, Input, OnInit } from '#angular/core';
#Directive({
selector: '[appCloseOnTimeout]'
})
export class CloseOnTimeoutDirective implements OnInit {
#Input() appCloseOnTimeout: number;
#HostBinding('close') close: CloseEvent;
constructor () {}
ngOnInit () {
setTimeout (() => this.close(), this.appCloseOnTimeout);
}
}
I want to be able to use the directive like this:
<ngb-alert
[dismissible]="alert.dismissible"
[type]="alert.type"
(close)="onClose(i)"
[appCloseOnTimeout]="1000"
>
What's the best way to access the host element's close event? I've tried using an ElementRef instead but still can't find a way to access the events.
Use something like...
import { Directive, ElementRef, HostBinding, Input, OnInit, Output, EventEmitter } from '#angular/core';
#Directive({
selector: '[appCloseOnTimeout]'
})
export class CloseOnTimeoutDirective implements OnInit {
#Input() appCloseOnTimeout: number;
#Output() close:EventEmitter<any> = new EventEmitter();
constructor () {}
ngOnInit () {
setTimeout (() => this.closeWrapp(), this.appCloseOnTimeout);
}
closeWrapp(){
this.close.emit()
}
}
Why not EventEmitter?
import {
Directive,
ElementRef,
HostBinding,
Input,
OnInit,
Output,
EventEmitter
} from '#angular/core';
#Directive({
selector: '[appCloseOnTimeout]'
})
export class CloseOnTimeoutDirective implements OnInit {
#Input() appCloseOnTimeout: number;
#Output() close: EventEmitter = new EventEmitter();
constructor() {}
ngOnInit() {
setTimeout(() => this.onClose(), this.appCloseOnTimeout);
}
onClose() {
console.log('local close');
this.close.emit();
}
}
Under my Angular app , i ve done a Custom directive:
#Directive({
selector: '[appCustomEdit]'
})
export class CustomEditDirective implements OnChanges {
#Input() appCustomEdit: boolean;
private element: any;
constructor(private el: ElementRef, private renderer: Renderer2) {
this.element = el.nativeElement;
}
ngOnChanges(changes: SimpleChanges) {
if (changes.appCustomEdit.currentValue) {
const btnElement = (<HTMLElement>this.element)
.querySelector('.dx-link-save');
this.renderer.listen(btnElement, 'click', () => {
alert('Buton was clicked')
});
}
}
}
in myComponent.html i m using this directive :
<div>
<input [appCustomEdit]=true></input>
</div>
i need now to implement some event / observable outputed from the directive so that i can subscribe to it in myComponent.ts and make some actions.
I wonder how to do it ?
Suggestions ?
Well, direct answer to your question would be something like the following:
import {Directive, EventEmitter, HostListener, Output} from '#angular/core';
#Directive({
selector: '[appCustomInput]'
})
export class CustomInputDirective {
#Output()
myCustomEvent = new EventEmitter();
#HostListener('click')
onClick() {
this.myCustomEvent.emit();
}
}
And then use it like this:
<div>
<input appCustomInput (myCustomEvent)="onMyCustomEvent()"></input>
</div>
However, it is not clear what are you trying to achieve with this, so I cannot really say if this is the way to go or not.
Angular2 (2.0.0-rc.4)
I use Bootstrap's Tooltip, Tooltip need execute follow javascript when ready:
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
In Angular2,how to execute it?
That worked for me:
import { Directive, ElementRef, Input, HostListener, OnDestroy } from '#angular/core';
declare var $: any;
#Directive({
selector: '[appTooltip]'
})
export class TooltipDirective implements OnDestroy {
#Input()
public appTooltip: string;
constructor(private elementRef: ElementRef) { }
#HostListener('mouseenter')
public onMouseEnter(): void {
const nativeElement = this.elementRef.nativeElement;
$(nativeElement).tooltip('show');
}
#HostListener('mouseleave')
public onMouseLeave(): void {
const nativeElement = this.elementRef.nativeElement;
$(nativeElement).tooltip('dispose');
}
ngOnDestroy(): void {
const nativeElement = this.elementRef.nativeElement;
$(nativeElement).tooltip('dispose');
}
}
registering:
Importing it in in app.module.ts
Adding it in declarations on #NgModule (file app.module.ts)
And using it like this:
<button title="tooltip tilte" [appTooltip]></button>
<div data-toggle="tooltip" #tooltip></div>
class MyComponent {
#ViewChild('tooltip') tooltip:ElementRef;
ngAfterViewInit() {
this.tooltip.nativeElement.tooltip();
}
}