How to add mattooltip by custom directive in Angular - javascript

I am creating a custom directive called TooltipDirective whihc is going to add matTooltip to every host element, code is like below
import { Directive, ElementRef, Input, OnInit, Renderer } from '#angular/core';
#Directive({
selector: '[tooltip]'
})
export class TooltipDirective implements OnInit
{
#Input() tooltip: string;
constructor(private hostElement: ElementRef, private renderer: Renderer)
{
}
ngOnInit()
{
this.renderer.setElementAttribute(this.hostElement.nativeElement, 'matTooltip', this.tooltip);
}
}
In my html I have two elements to compare the result
<i class="material-icons" tooltip="Test Tooltip">reply_all</i>
<i class="material-icons" matTooltip="Test Tooltip">reply_all</i>
in the result html tooltip and mattooltip attributes are added but it doesn't show the tooltip.
and rendered html is like below
<i _ngcontent-c10="" class="material-icons" tooltip="Test Tooltip" mattooltip="Test Tooltip" ng-reflect-tooltip="Test Tooltip">reply_all</i>
<i _ngcontent-c10="" class="material-icons" mattooltip="Test Tooltip" aria-describedby="cdk-describedby-message-1" cdk-describedby-host="" ng-reflect-message="Test Tooltip">reply_all</i>
I tried adding other extra attributes but still doesn't work.

The other answer and comments are correct, btw finally I made it like this and it's working
import { Directive, ElementRef, Inject, Input, NgZone, Optional, ViewContainerRef } from '#angular/core';
import
{
MAT_TOOLTIP_DEFAULT_OPTIONS,
MAT_TOOLTIP_SCROLL_STRATEGY,
MatTooltip,
MatTooltipDefaultOptions
} from '#angular/material/tooltip';
import { AriaDescriber, FocusMonitor } from '../../../../../node_modules/#angular/cdk/a11y';
import { Directionality } from '../../../../../node_modules/#angular/cdk/bidi';
import { Overlay, ScrollDispatcher } from '../../../../../node_modules/#angular/cdk/overlay';
import { Platform } from '../../../../../node_modules/#angular/cdk/platform';
#Directive({
selector: '[tooltip]',
exportAs: 'tooltip'
})
export class TooltipDirective extends MatTooltip
{
#Input()
get tooltip()
{
return this.message;
}
set tooltip(value: string)
{
this.message = value;
}
constructor(
_overlay: Overlay,
_elementRef: ElementRef,
_scrollDispatcher: ScrollDispatcher,
_viewContainerRef: ViewContainerRef,
_ngZone: NgZone,
_platform: Platform,
_ariaDescriber: AriaDescriber,
_focusMonitor: FocusMonitor,
#Inject(MAT_TOOLTIP_SCROLL_STRATEGY) _scrollStrategy: any,
#Optional() _dir: Directionality,
#Optional() #Inject(MAT_TOOLTIP_DEFAULT_OPTIONS)
_defaultOptions: MatTooltipDefaultOptions)
{
super(
_overlay,
_elementRef,
_scrollDispatcher,
_viewContainerRef,
_ngZone,
_platform,
_ariaDescriber,
_focusMonitor,
_scrollStrategy,
_dir,
_defaultOptions
);
}
}

This works for me. In my case I needed to have some checks before displaing the tooltip on 'mouseover' event.
import { Directive, ElementRef, HostListener, Input } from '#angular/core';
import { MatTooltip } from '#angular/material/tooltip';
#Directive({
selector: '[customTooltip]',
providers: [MatTooltip]
})
export class CustomTooltipDirective {
#Input() tooltipText: string;
constructor(private elementRef: ElementRef, private tooltip: MatTooltip) {}
#HostListener('mouseover') mouseover() {
this.tooltip.message = this.tooltipText;
this.tooltip.show();
}
#HostListener('mouseleave') mouseleave() {
this.tooltip.hide();
}
}

There's no way to do it in Angular. Keep an eye on this, so if maybe Angular guys will do it in case they start doing meaningful work.
Your other option is to create a dynamic component for this situtation which sucks for this kind of little thing. I'm not sure but it may break your AOT.

Related

CSS styles doesn't work for Html added by innerHtml in Angular 2+ directive

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>';

Angular - Structural directive with embedded view does not pass children to ng-template

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

Trigger event on host from directive

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();
}
}

Angular : Output a callback of my Custom directive event and subscribe to it in my component

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.

In Angular2 With Bootstrap - Tooltip, Tooltip Need setup by executing javascript, How to do it?

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();
}
}

Categories

Resources