Angular get access to value of a directive in directive - javascript

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

Related

custom if directive does not show expected results

in this tutorial
https://www.sitepoint.com/practical-guide-angular-directives/
i am learning how to create a customised directive. i followed the steps as shown in the code posted below, but despite added the exact code as explained in the aforemenrtioned website, when i run the command
ng serve --open
i get something as shown in the image posted below.
please let me know why myCustomIf is not working. i say that myCustomIf is not working because what i got on the localhost:4200 is something as shown in the image posted
please let me know how to make the myCustomIf works as explained in the tutorial in the above posted link
app.component.ts:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'ngDirective1';
name = 'Angular';
condition = false;
}
app.myCustomeIfDirective.ts:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.module:
import { Directive, Input, TemplateRef, ViewContainerRef } from '#angular/core';
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
constructor(private templateRef: TemplateRef<any>,private viewContainer: ViewContainerRef){ }
#Input()
setMyCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
app.component.html:
<h1 my-error>Hello {{name}}</h1>
<h2 *myCustomIf="condition">Hello {{name}}</h2>
<button (click)="condition = !condition">Click</button>
image:
If you open console it should show smth like:
NG0303: Can't bind to 'myCustomIf' since it isn't a known property of
'h2'
An Angular structural directive, that is written in a short syntax(with *) and that takes one input or more inputs, must have an #Input with the same name as directive's attribute selector(other inputs follow another rule described here What is the exact grammar for Angulars structural directives), e.g.:
#Directive({
selector: '[anyAttr]'
})
export class MyCustomeIfDirective{
#Input()
anyAttr: any;
or
#Directive({
selector: '[anotherAttr]'
})
export class MyCustomeIfDirective{
#Input()
set anotherAttr(val: any) {}
Why is it so?
That's because *ngIf is just a shortcut for expanded version:
<ng-template [ngIf]="...">...
or
*anyAttr => <ng-template [anyAttr]="...">...
Now, let's look at your code:
#Directive({
selector: '[myCustomIf]'
})
export class MyCustomeIfDirective{
#Input()
setMyCustomIf(condition : boolean) {
Several things to notice:
setMyCustomIf is just a method in your case
if you convert it to a setter set MyCustomIf then MyCustomIf doesnt match myCustomIf because js is case-sensitive.
The solution is:
#Input()
set myCustomIf(condition : boolean) {
Ng-run Example
in your directive (app.myCustomeIfDirective.ts), you need to match the name of your input to the name of the directive (because the condition is passed with that attribute):
#Input("myCustomIf")
set myCustomIf(condition : boolean) {
if(condition) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
(note you can also change the name of the function to match the directive name)
stackblitz demo

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

Access text (not instance of another component) with ContentChild

How can I access a string of text given within the tags of a component
<my-custom-component>THIS TEXT</my-custom-component>
Within a template, I can use ng-content, or if it is an instance of some other class I can access it within the component definition like demonstrated in these examples. However I am interested in detecting if there is a string of text there or not, which I believe would make providedText undefined. However, I am always getting undefined.
#ContentChild(Element, { static: true }) providedText: Text | undefined;
I have tried Text as the first element passed to #ContentChild. Passing any will not work (I don't know why).
StackBlitz
I am interested mostly in finding if there is a string or undefined, but am also curious why ContentChild(Text... isn't working.
Edit:
I have added a potential solution, but it seems pretty imperfect, so I hope something better comes along.
Edit 2:
I now understand that #ContentChild is not a mechanism for selecting whatever native HTML I want without wiring it up to Angular’s dependency graph with a ref, directive, etc.
I am still curious if my proposed solution below is a bad idea for any reason.
My solution for now (since I wish to capture all transcluded content) is to wrap ng-content in a containing element, then get its innerText.
#Component({
selector: "app-parent",
template: `
<span #transcludedContainerRef>
<ng-content></ng-content>
</span>
`
})
export class ParentComponent implements AfterViewInit {
#ViewChild("transcludedContainerRef", { static: false })
transcludedContainerRef: ElementRef | undefined;
buttonText: string;
ngAfterViewInit() {
const isButtonTextPresent = this.transcludedContainerRef.nativeElement
.innerText;
if (isButtonTextPresent) {
console.log(isButtonTextPresent); // successfully logs content
}else {
console.log('No text set');
}
}
}
It does feel hacky, but it works. I am holding out for something better.
it's difficult if I don't know about your <my-custom-component>
In general if your custom component it's only
<ng-content></ng-content>
You can inject in constructor the elementRef
constructor(public el:ElementRef){}
From a parent
<hello >
Start editing to see some magic happen :)
</hello>
You can use
#ViewChild(HelloComponent,{static:false}) helloComponent:HelloComponent
click()
{
console.log(this.helloComponent.el.nativeElement.innerHTML)
}
If your component has any variable -or ViewContent-, you can access this variables in a similar way
So the other way to read the inner text from the component is that child component emit the value whatever it get's as input from other component. See below:
hello.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '#angular/core';
#Component({
selector: 'hello',
template: `<h1>Hello {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`]
})
export class HelloComponent implements OnInit {
#Input() name: string;
#Output() innerText: EventEmitter<string> = new EventEmitter();
ngOnInit() {
this.innerText.emit(this.name);
}
}
app.component.ts
import { Component, ContentChild, AfterContentInit, OnInit } from "#angular/core";
#Component({
selector: "app-parent",
template: "content from <code>app-parent</code>"
})
export class ParentComponent implements AfterContentInit {
#ContentChild(Element, { static: true }) providedText: Text | undefined;
ngAfterContentInit() {
console.log("ngAfterContentInit Content text: ", this.providedText);
}
}
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
_innerText: string;
ngAfterContentInit() {}
get childContent(): string {
return this._innerText;
}
set childContent(text) {
this._innerText = text;
}
innerTextFn(innertext: string) {
this.childContent = innertext;
console.log('Event: ', innertext);
}
}
app.component.html
<hello name="{{ name }}" (innerText)="innerTextFn($event)"></hello>
<app-parent>This is the content text</app-parent>
Here is stackblitz url to check: https://stackblitz.com/edit/angular-bacizp
I hope this may helpful for you and if yes then accept this as correct answer.

Angular - How can I toggle the visibility of an element in a component from another component?

I have the following scenario in my Angular app:
A component MainDashboardComponent that is visible when I have the route /. Obviously I have the <router-outlet> tag in my app.component.html file, which looks like this:
<app-side-menu></app-side-menu>
<div class="main-container">
<div class="content">
<router-outlet></router-outlet>
</div>
</div>
As you can see I have a SideMenuComponent I use to have a side menu on all my routes. In MainDashboardComponent I have a method that for some reason needs to toggle a chat element that is situated on the side menu.
Inside the SideMenuComponent I have a method that handles the visibility toggle for the chat element and it works as expected. How can I call this method from my MainDashboardComponent and toggle the chat element from there?
What I tried with no success
I tried to inject the SideMenuComponent inside my MainDashboardComponent but, though the method toggleChat() is called, the element doesn't change it's visibility. Looks like I have a kind of multiple instance of the same component I guess...
Can you please help me with this? Thank you!
MainDashboardComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-main-dashboard',
templateUrl: './main-dashboard.component.html',
styleUrls: ['./main-dashboard.component.scss']
})
export class MainDashboardComponent implements OnInit {
constructor() { }
ngOnInit() {}
setFocus(id) {
// here I'd like to call SideMenuComponent togglechat() ...
}
}
SideMenuComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-side-menu',
templateUrl: './side-menu.component.html',
styleUrls: ['./side-menu.component.scss']
})
export class SideMenuComponent implements OnInit {
showChat: boolean;
constructor() {
this.showChat = false;
}
ngOnInit() {
}
toggleChat() {
this.showChat = !this.showChat;
}
}
To communicate between different components, there are different ways.
If you want to communicate between parent and child component, you can use EventEmitter to emit event from child component and handle the event in your parent component
If you want to communicate between any components, you can use Service and implement communication with the help of EventEmitter or Subject/BehaviorSubject
In your case, we can create a service, myService.ts and declare and eventEmitter
.service.ts
#Injectable()
export class AppCommonService {
toggle : EventEmitter<boolean> = new EventEmitter<boolean>()
}
mainDashboard.component.ts
constructor(private myService : myService){}
chatStatus : boolean = false;
ngOnInit(){
this.myService.toggle.subscribe(status=>this.chatStatus = status);
}
toggleChat(){
this.myService.toggle.emit(!this.chatStatus);
}
sideMenu.component.ts
constructor(private myService : myService){}
chatStatus : boolean = false;
ngOnInit(){
this.myService.toggle.subscribe(status=>this.chatStatus = status);
}
Generally this is the domain of a service!
Just create a service and add the "showCat" property.
Inject the service into both components
Alter SideMenuComponent to:
toggleChat() {
this.myService.showChat = !this.myService.showChat;
}
Alter MainDashboardComponent, also use this.myService.showChat to show / hide your chat window
Service TS
#Injectable()
export class MyService{
showCat:boolean = true
}
MainDashboardComponent
toggleChat() {
this.myService.showChat = !this.myService.showChat;
}
SideMenuComponent
chatVisiblity = this.myService.showCat //<-- bind this to the element attribute
You could efficiently use child to parent communication in this scenario. You'll need to create a custom event using angular's EventEmitter in your SideMenuComponent and use it in your MainDashboardComponent.
So, here is some code that may help you -
// SideMenuComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-side-menu',
templateUrl: './side-menu.component.html',
styleUrls: ['./side-menu.component.scss']
})
export class SideMenuComponent implements OnInit {
#Output() valueChange = new EventEmitter();
showChat: boolean;
constructor() {
this.showChat = false;
}
ngOnInit() {
}
toggleChat() {
this.showChat = !this.showChat;
this.valueChange.emit(this.showChat);
}
}
// MainDashboardComponent
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-main-dashboard',
template: `<app-side-menu (valueChange)='setFocus($event)'></app-side-menu>`
styleUrls: ['./main-dashboard.component.scss']
})
export class MainDashboardComponent implements OnInit {
constructor() { }
ngOnInit() { }
setFocus(event) {
// check for required input value
console.log(event);
}
}
Refer these tutorials if required -
https://dzone.com/articles/understanding-output-and-eventemitter-in-angular,
https://angular-2-training-book.rangle.io/handout/components/app_structure/responding_to_component_events.html

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

Categories