Wrapping an Angular 2+ directive with another directive - javascript

I'm trying to reduce clutter in my (Angular 7) app by creating a directive which takes a simplified set of parameters (such as a user ID) and displays a ng-bootstrap popover.
I'd like the directive to work as similarly as possible to a normal ng-bootstrap popover, except that it's created using the custom directive instead. I know I could do something similar with a component, but I'm planning on using this directive on enough different elements that it wouldn't be feasible.
Is it possible to wrap directives like this in Angular 2+, and if so what would the best approach be to making this happen?
I've created a StackBlitz with what I've created so far here:
import { Directive, ElementRef, OnInit, Input } from '#angular/core';
#Directive({
selector: '[app-custom-directive]'
})
export class CustomDirective implements OnInit {
private element: HTMLInputElement;
#Input() parameter: string = 'Parameter';
constructor(private elRef: ElementRef) {
this.element = elRef.nativeElement;
}
ngOnInit() {
this.element.onclick = () => {
alert('This should open a popover containing the directive parameter "' + this.parameter + '". But how?')
};
}
}

1) First of all, you should never do the
this.element.onclick = () => {
Use #HostListener instead. It's angular-way to listen for events in angular on the Directives.
2) You really need a component here which will have a directive and the input you need.
3) I don't know if it will work but you can at least try to extend a NgbPopover directive:
export class CustomDirective extends NgbPopover {
// private element: HTMLInputElement;
constructor(
private _elementRef: ElementRef<HTMLElement>,
private _renderer: Renderer2,
injector: Injector,
componentFactoryResolver: ComponentFactoryResolver,
viewContainerRef: ViewContainerRef,
config: NgbPopoverConfig,
private _ngZone: NgZone,
#Inject(DOCUMENT) private _document: any
) {
super(_elementRef, _renderer, injector, componentFactoryResolver, viewContainerRef, config, _ngZone, _document);
}

Related

Angular get access to value of a directive in directive

I want to create a structural directive that queries all anchors that has "routerLink" directive in the app and and access the value of the routerLink.
I tried to implement it with this way but without success:
#Directive({
selector: '[appShowByRole], a[routerLink]'
})
export class ShowByRoleDirective implements OnInit{
constructor(
private readonly elmRef: ElementRef,
#Optional() #Inject(forwardRef(() => RouterLink)) private routerLink: RouterLink,
) {
}
ngOnInit(): void {
if (this.routerLink) {
debugger;
}
}
}
You can use Input to receive any attribute from template.
#Directive({
selector: '[appShowByRole], a[routerLink]'
})
export class ShowByRoleDirective implements OnInit {
#Input('routerLink') link: any;
....
}
For a tags the routerLink directive class name is RouterLinkWithHref
Here you have both directives decorators extracted from the source code
RouterLinkWithHref
#Directive({
selector: 'a[routerLink],area[routerLink]',
standalone: true}
)
export class RouterLinkWithHref implements OnChanges, OnDestroy {
...
RouterLink
#Directive({
selector: ':not(a):not(area)[routerLink]',
standalone: true,
})
export class RouterLink implements OnChanges {
...
I don't know exactly whats the aim of your directive, but you don't need to include the routerlink in you directive's selector to get the ref. Neither should you need to use forwardRef.
cheers

How to build a directive to disable all type of material controls?

I want to build an angular directive which when applied on any control(material controls) like matButton/matSelect/matAutcomplete etc.., should disable control based on a condition. Any help.?
This would be the process:
Create your directive using the angular cli
ng generate directive customMatDisable
This will create a new directive file for you .directive.ts
Inside the directive file manipulate the element like so
import { Directive, ElementRef } from '#angular/core';
#Directive({
selector: '[customMatDisable]'
})
export class CustomMatDisableDirective {
constructor(el: ElementRef) {
if(//condition here){
el.nativeElement.disabled = true;
}
}
}
and use it like below:
<button customMatDisable mat-raised-button >Button Text</button>
Update
Since it seems that the material directives override our disabled status since its initialization takes place after our directive executes. One workaround would be to do it like this:
import { Directive, ElementRef } from '#angular/core';
#Directive({
selector: '[customMatDisable]'
})
export class CustomMatDisableDirective {
constructor(el: ElementRef) {
if(//condition here){
setTimeout(()=>{
el.nativeElement.disabled = true;
el.nativeElement.classList.add("mat-button-disabled")
},1) //execute after 1 ms
}
}
}
Another aproach is inject in constructor buttons,matInput.. and disable it
export class CustomMatDisableDirective implements OnInit {
constructor(
#Optional() #Self() private control:NgControl,
#Optional() #Self() private button:MatButton,
#Optional() #Self() private input:MatInput,
#Optional() #Self() private select:MatSelect,
#Optional() #Self() private toogle:MatDatepickerToggle<any>,
) {}
ngOnInit() {
if (this.control && this.control.control)
this.control.control.disable()
if (this.button)
this.button.disabled=true
if (this.input)
this.input.disabled=true;
if (this.select)
this.select.disabled=true;
if (this.toogle)
this.toogle.disabled=true;
}
See stackblitz

How to detect DOM changes using a class Selector Directive

Create a directive that applied certain functionality to all the elements that had the "active" class.
However, if at run time it added the "active" class to an existing DOM element, the functionality of the directive was not applied to it.
import { Directive, Renderer2, ElementRef, OnInit, HostListener } from '#angular/core';
#Directive({
// tslint:disable-next-line:directive-selector
selector: '.active'
})
export class RemoveActivesDirective implements OnInit {
el: any;
constructor(private element: ElementRef, private renderer: Renderer2) {
this.el = element.nativeElement;
}
ngOnInit(): void {
}
#HostListener('click', ['$event']) removeActives() {
console.log('Eliminando elemento', event);
this.renderer.selectRootElement(this.el).remove();
}
}
How can I tell the board that there have been changes in the html?
How can I make the directive work with the elements that acquire the class at run time?
Thanks

Multiple instance of angular 4 directive called from a component mesed up the input values

I have a component in angular 4 that is called three times. In template metadata I have a div with a directive with some bindings like this.
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {};
this.cOptions = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
//this.report.opt is binded to a component when is instantiated.
//this.gServ.objectMerge is a function that merge the two objects
}
}
this.cOptions change for every instance of the component, then in the directive I have this:
import { Directive, ElementRef, HostListener, Input, OnInit } from '#angular/core';
#Directive({
selector: '[gDirective]'
})
export class SGDirective implements OnInit {
public _element: any;
#Input() public cOptions: string;
constructor(public element: ElementRef) {
this._element = this.element.nativeElement;
}
ngOnInit() {
console.log(this.cOptions);
}
}
The problem is that console.log(this.cOptions); always print the same object, even when component set cOptions with diferent values in ngOnInit method of the compnent.
Do you have some idea what is wrong?
Your component property binding [cOptions]="dataChart" doesn't look good, reason being your dataChart is not even defined. it should be like [DIRECTIVE_PROPERTY]="COMPONENT_PROPERTY" and your COMPONENT_PROPERTY is not even defined in SGComponent component class.
Your component class should be something like this:
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="Options">`
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = {};
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
#Ashwani points out a valid problem with your code. The way your template is wiring things up, nothing will ever be passed to the SGDirective input.
Another potential problem you could be running into has to do with the gServ code. If gServ is a singleton (which is probably the case) and it is returning the same object to each of the SGComponents, then all the SGDirectives will have the same value. A simple way to test this is to put {{Options | json}} in the SGComponent template.
To create a new instance of the gServ service for each SGComponent you can add a providers array to the #Component metadata. It would look like this:
import {gServ} from '../gServ.service';
#Component({
selector: 'sr-comp',
template: `{{Options | json}}<div gDirective [cOptions]="Options"></div>`
providers: [gServ],
})
export class SGComponent implements OnInit {
#Input('report') public report: IReportInstance;
Options:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.Options = this.gServ.objectMerge(this.gServ.defaultOpt, this.report.opt);
}
}
You have probably the same return/value at this.gServ.objectMerge) (you can test it wihtout calling the service, and passing each one one different objet make by you)
#import {gServ} from '../gServ.service';
#Component: ({
selector: 'sr-comp',
template: `<div gDirective [cOptions]="dataChart">`
})
export class SGComponent implements OnInit {
//#Input('report') public report: IReportInstance;
cOptions:any;
constructor(private gServ: gServ) {
}
ngOnInit(){
this.cOptions = {nicolas: 'nicolas1'}; //change this in the next component that use the directive
}
}
If that is the case, your problem is that gServ is provide at the same rootComponent. with angular, service provider at the same rootComponent are singleton.
And use the same type in your directive and your component!!

Angular2: making an iframe src safe using a directive

I am trying to apply a sanitized src attribute to an iframe, directly it works fine, but when putting it all in an attribute directive, it refuses to play ball. Here is the directive code, and the error message that appears
import { OnInit, Directive, ElementRef, Input, Renderer } from '#angular/core';
import { DomSanitizer, SafeResourceUrl} from '#angular/platform-browser';
#Directive({
selector: '[resume]'
})
export class ResumeDirective implements OnInit {
#Input('resume') inputLink: string;
constructor(private _sanitizer: DomSanitizer, private el: ElementRef, private render: Renderer) {
}
ngOnInit(): void {
let _url: string = this.inputLink + '#zoom=100';
let resumeUrl: SafeResourceUrl = this._sanitizer.bypassSecurityTrustResourceUrl(_url);
// this.el.nativeElement.src = resumeUrl.toString(); // same result
this.render.setElementProperty(this.el.nativeElement, 'src', _url);
// using 'srcdoc' or setElementAttribute brings same results
}
}
I get error: SafeValue must use [property]=binding: /theurl/x.pdf#zoom=100 (see http://g.co/ng/security#xss)
You can try #HostBinding() - not sure if this will work though
#Directive({
selector: '[resume]'
})
export class ResumeDirective implements OnInit {
#Input('resume') inputLink: string;
constructor(private _sanitizer: DomSanitizer, private el: ElementRef, private render: Renderer) {
}
#HostBinding('src')
resumeUrl:any;
ngOnInit(): void {
let _url: string = this.inputLink + '#zoom=100';
this.resumeUrl = this._sanitizer.bypassSecurityTrustResourceUrl(_url);
}
}
this.render.setElementProperty doesn't care about sanitized values, it just calles to DOM APIs and passes the sanitized value as-is.

Categories