I'm trying to prevent click event on disabled buttons, in other words, prevent some user who removes the disabled attribute to call some action.
For now, I have the following code to do this:
<button [disabled]="someCondition" (click)="executeAction()">Execute action</button>
executeAction(): void {
if (this.someCondition) return;
// ...
}
Works, but it isn't a good solution as I have to do it for ALL buttons in my app (and believe me, it's easy to forgot to do this and even a Linter can't help me here).
Looking for a more robust solution, I thought that directive could help me:
import { Directive, HostListener, Input, Renderer2, ElementRef } from '#angular/core';
#Directive({
selector: 'button'
})
export class ButtonDirective {
#Input() set disabled(value: boolean) {
this._disabled = value != null;
this.renderer2.setAttribute(this.elementRef.nativeElement, 'disabled', `${this._disabled}`);
}
private _disabled: boolean;
constructor(
private readonly elementRef: ElementRef,
private readonly renderer2: Renderer2
) { }
#HostListener('click', ['$event'])
onClick(mouseEvent: MouseEvent) {
// nothing here does what I'm expecting
if (this._disabled) {
mouseEvent.preventDefault();
mouseEvent.stopImmediatePropagation();
mouseEvent.stopPropagation();
return false; // just for test
}
}
}
<button [disabled]="someCondition" (click)="executeAction()">Execute action</button>
executeAction(): void {
console.log('still being called');
}
...however it does absolutely nothing. It doesn't prevent the click event. Is there any solution that I don't have to control the action itself in its call?
STACKBLITZ
This is a workaround with CSS which cheaper than scripts.
You easily could use
pointer-events: none;
In this case, the button will not be clickable.
As a UX enhance you could also wrap your button inside a div and give this div a CSS property
cursor: not-allowed;
Which will show the blocked circle icon instead of normal mouse view when hover.
In your directive, you can do something like this. You can achieve it by adding an event listener to parent in the capturing phase.
ngOnInit() {
this.elementRef.nativeElement.parentElement.addEventListener('click',(e) => {
if(this._disabled && e.target.tagName === 'BUTTON') {
e.stopImmediatePropagation();
e.stopPropagation();
}
}, true);
}
You can remove the listener in onDestroy
Prevent click event on disabled buttons
If the disabled attribute is there the click will not happen.
When user decides to use devtools
However if the user edits the HTML and removes the disabled attribute manually, then click will happen. You can try and do the check as you have suggested, but the browser is an unsafe environment. The user will still be able to execute any code on the webpages behalf irrespective of any frontend checks you might put in.
Related
Updated Question for more Clarity:
Need to display some texts and links as innerHTML(data from service/DB) in the Angular HTML and when user clicks, it should go to Typescript and programmatically navigates by router.navigate
Also, How to add DomSanitizer from #ViewChild/ElementRef
Added all example in below code
Here is the updated stackblitz code
As shown in screenshot from angular.io some texts and some links
Sorry, I didn't realize you answered my comment. Angular routing is not secondary, if you don't use Angular modules you'll end up with just an HTML/CSS/Typescript application. you need at least the RouterModule for Angular to be able to use routing and hence, do what it's supposed to with the DOM.
First:
You are not importing RouterModule
solution:
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([]) // this one
]
Second:
You can't bind Angular events through innerHTML property
fix:
Make use of #ViewChild directive to change your innerHTML property and manually bind to the click event, so change in your app.component.html from
<div id="box" [innerHTML]="shouldbedivcontent" ></div>
to
<div #box id="box"></div>
Now, in your app.component.ts, add a property to hold a reference to that "box" element so you can later make some changes to the dom with it:
#ViewChild('box') container: ElementRef;
Implement AfterViewInit, that hook is where you will be able to actually handle your container, if you try using it for example in OnInit you'd get undefined because that component's html is not in the dom yet.
export class AppComponent implements AfterViewInit {
and
ngAfterViewInit() {
this.container.nativeElement.innerHTML = this.shouldbedivcontent;
this.container.nativeElement.addEventListener('click',
() => this.goto('bar')
);
}
change shouldbedivcontent property from:
'1) this is a click
<a (click)="goto("bar")">Click</a><br>
2)this is with routerlink
<a routerLink="" (click)="goto("bar")">Click</a><br>
3)This only works with href
bar and test'
to
'1) this is a click
<a id="link_1">Click</a><br>
2)this is with routerlink
<a [routerLink]="" (click)="goto(\'bar\')">Click</a><br>
3)This only works with href
bar and test'
And even so you'd still not get the default anchor style unless you apply some styling yourself.
Third
You are not HTML sanitizing, which could be dangerous. read more here
MY SUGGESTION:
Seems like a lot to do for you and a lot to read for someone else working alongside you for something you could easily do like in the example below!
Move your html to your app.component.html:
<div id="box">
1) this is a click
<a (click)="goto('bar')">Click</a><br>
2)this is with routerlink
<a routerLink="" (click)="goto('bar')">Click</a><br>
3)This only works with href
bar and test
</div>
<p>Below is actual content</p>
You'll notice that everything works now, except the anchor without routerLink or href, because that's not a link.
EDIT:
Looking at the new stackblitz, i suggest a change of approach, binding to innerHTML is ok when working with plain text or even some simple html but not a great choice to bind events or routing logic.
Angular's Renderer2 provides with a bunch of methods to dyncamically add elements to the DOM. With that on the table, you just need a little effort to take that simple html you get from your backend and turn it into something like (paste this property in your code to test it along the rest of the code provided below):
public jsonHTML = [
{
tagName: '',
text: 'some text with click ',
attributes: {
}
},
{
tagName: 'a',
text: 'bar',
attributes: {
value: 'bar' // goto parameter
}
},
{
tagName: '',
text: ' some more text with click ',
attributes: {
}
},
{
tagName: 'a',
text: 'foo',
attributes: {
value: 'foo' // goto parameter
}
}
]
Once you have it, it's way easier to create all of those elements dynamically:
this is for the code in your Q1:
Inject Renderer2 with private r2: Renderer2
And replace the Q1 related code in AfterViewInit hook to:
const parent = this.r2.createElement('div'); // container div to our stuff
this.jsonHTML.forEach((element) => {
const attributes = Object.keys(element.attributes);
const el = element.tagName && this.r2.createElement(element.tagName);
const text = this.r2.createText(element.text);
if (!el) { // when there's no tag to create we just create text directly into the div.
this.r2.appendChild(
parent,
text
);
} else { // otherwise we create it inside <a></a>
this.r2.appendChild(
el,
text
);
this.r2.appendChild(
parent,
el
);
}
if (attributes.length > 0) {
attributes.forEach((name) => {
if (el) {
this.r2.setAttribute(el, name, element.attributes[name]); // just the value attribute for now
if (name === 'value') {
this.r2.listen(el, 'click', () => {
this.goto(element.attributes[name]); // event binding with property "value" as parameter to navigate to
})
}
} else {
throw new Error('no html tag specified as element...');
}
})
}
})
this.r2.appendChild(this.container.nativeElement, parent); // div added to the DOM
No html sanitizer needed and no need to use routerLink either just inject Router and navigate to the route you want! Make improvements to the code t make it fit your needs, it should be at least a good starting point
Good Luck!
You have a css problem.
looks like a link
<a [routerLink]="something"></a> looks like a link, because if you inspect the HTML it actually gets an href property added because of routerLink
<a (click)="goTo()"></a> does NOT look like a link, because there is no href
Chrome and Safari default user agents css will not style <a> without an href (haven't confirmed Firefox but I'm sure its likely). Same thing for frameworks like bootstrap.
Updated stackblitz with CSS moved to global, not app.css
https://stackblitz.com/edit/angular-ivy-kkgmkc?embed=1&file=src/styles.css
This will style all links as the default blue, or -webkit-link if that browser supports it. It should be in your global.css file if you want it to work through the whole app.
a {
color: rgb(0, 0, 238);
color: -webkit-link;
cursor: pointer;
text-decoration: underline;
}
this works perfectly for me :D
#Directive({
selector: "[linkify]",
})
// * Apply Angular Routing behavior, PreventDefault behavior
export class CustomLinkDirective {
#Input()
appStyle: boolean = true;
constructor(
private router: Router,
private ref: ElementRef,
#Inject(PLATFORM_ID) private platformId: Object
) {}
#HostListener("click", ["$event"])
onClick(e: any) {
e.preventDefault();
const href = e.target.getAttribute("href");
href && this.router.navigate([href]);
}
ngAfterViewInit() {
if (isPlatformBrowser(this.platformId)) {
this.ref.nativeElement.querySelectorAll("a").forEach((a: HTMLElement) => {
const href = a.getAttribute("href");
href &&
this.appStyle &&
a.classList.add("text-indigo-600", "hover:text-indigo-500");
});
}
}
}
HOW I USE IT
<p linkify
class="mt-3 text-lg text-gray-500 include-link"
[innerHtml]="apiSectionText"
></p>
result
Is it possible to call a function when the button is created.
So that I check in the function if a button has to be disabled or not
thx a lot!
You can create a directive to detect if element has created snd then run a function base on that
import { Directive , Output ,EventEmitter } from '#angular/core';
#Directive({
selector: '[created]'
})
export class CreatedDirective {
#Output() created:EventEmitter<any> = new EventEmitter();
ngAfterViewInit() {
this.created.emit()
}
}
Demo 🎬
Another method by using ViewChildren or ViewChild check my answer here
You can use the #ViewChild() and check the value to see if the button exists.
Something like:
In template:
<input type='button' #button />
In component:
#ViewChild('button') someButton;
In function:
if (this.someButton){
// do something with someButton
// You might want to do this.someButton.nativeElement and convert to HTMLelement to get the element as button
}
I am working with inputs but I am not really sure about how is the configuration of the navigation done (I guess that it are predefined behaviours).
I am not in the last input the enter key goes to the next one. This one is working as I want.
Nevertheless, when I am on the last input, when I press enter, it automatically clicks on the next button.
This is what I am trying to avoid. Is there any way to change this behaviour? Just to close the keyboard or to click on another button?
I have tried with keyup.enter and it pseudo works. It calls to the method but also clicks on the next button
HTML
<input
type="text"
class="form-control"
id="validationCustomSurname"
placeholder="e.g. Lopez"
required
(keyup.enter)="onNavigate(1, 'forward')"
[(ngModel)]="values.store.surname"
name="surname"
/>
This method should work on a phone, so I guess that keydown is not an option since $event.code does not give me any code in the phone.
Some time ago I make a directive see stackblitz that you apply in a div (or in a form) in the way
<form [formGroup]="myForm" (submit)="submit(myForm)" enter-tab>
Each input or button add a reference variable #nextTab like
<input name="input1" formControlName="input1" #nextTab/>
<button type="button" #nextTab/>
</form>
The directive use ContentChildren to add a keydown.enter to all the components that have #nextTab to focus to the next control
export class EnterTabDirective {
#ContentChildren("nextTab") controls: QueryList<any>
nextTab
constructor(private renderer: Renderer2, private el: ElementRef) {
}
ngAfterViewInit(): void {
this.controls.changes.subscribe(controls => {
this.createKeydownEnter(controls);
})
if (this.controls.length) {
this.createKeydownEnter(this.controls);
}
}
private createKeydownEnter(querycontrols) {
querycontrols.forEach(c => {
this.renderer.listen(c.nativeElement, 'keydown.enter', (event) => {
if (this.controls.last != c) {
let controls = querycontrols.toArray();
let index = controls.findIndex(d => d == c);
if (index >= 0) {
let nextControl = controls.find((n, i) => n && !n.nativeElement.attributes.disabled && i > index)
if (nextControl) {
nextControl.nativeElement.focus();
event.preventDefault();
}
}
}
})
})
}
Here's a very simple approach, with just a few lines of code:
First, in your Template when you dynamically create your Input elements: 1. populate the tabIndex attribute with a unique number, 2. populate a super-simple custom "Tag" Directive with the same unique number as the tabIndex, and 3. set up a Keydown "Enter" event listener:
Template:
<ng-container *ngFor="let row in data">
<input tabindex ="{{row[tabCol]}}" [appTag]="{{row[tabCol]}}" (keydown.enter)="onEnter($event)" . . . />
</ng-container>
In your component, your super-simple event-listener onEnter():
#ViewChildren(TagDirective) ipt!: QueryList<ElementRef>;
onEnter(e: Event) {
this.ipt["_results"][(<HTMLInputElement>e.target).tabIndex%(+this.ipt["_results"].length-1)+1].el.nativeElement.focus();
}
Note: The modulus (%) operation is just to make sure that if you're at the last Input, you'll get cycled back to the first input.
Super-simple, bare-minimum "Tag" Directive
import { Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appTag]'
})
export class TagDirective {
#Input('appTag') id: number;
constructor(public el: ElementRef) { }
}
There's probably even a way to get rid of the "Tag" `Directive altogether and make it even more simple, but I haven't had time to figure out how to do that yet . . .
I want to be able to add a scss class ("error") to an element, if a [disabled] button is clicked. How do i achieve this?
Angular2 disable button
the [disabled] attribute is required, so this is not a working solution.
<button [disabled]="!isTermAgreed || isLoading" (click)="payForStory(paymentObject.StoryId)">
scss class that a span element will recieve through [ngClass]:
&.error {
border: 2px solid red;
}
You cannot listen for click events on a disabled button. You could add a disabled class to the button and then listen for events as normal. Please review this stackblitz: https://stackblitz.com/edit/angular-3ksttt
Component:
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
isButtonClicked: boolean = false;
public disableButtonClick() {
this.isButtonClicked = true;
}
}
Template:
<button [ngClass]="isButtonClicked ? 'error' : ''" (click)="disableButtonClick()" class="disabled">BUTTON</button>
Create a boolean property in your component, I.e. activeClass, listen for a click event on your button to toggle it, and then simply use ngClass to assign the class depending on the boolean.
[ngClass]="'yourClass': activeClass"
edit
As correctly pointed out, you can't listen to events on a disabled button. A solution to this is to wrap the element with a div and listen to events on that element (see this answer for more details) or to use a class to simulate the disabled effect.
Whenever the button is disabled, a class of button:disabled will be added by default. So you can put your styles in the class. As shown:
button:disabled {
background-color: #97929248;
cursor: not-allowed;
}
There is no pretty way to do this in Angular. The way Angular is structured such that you shouldn't be manually checking the DOM except in rare circumstances. If you must do this, check out this link, this accepted answer has a solution that may be useful to you.
An alternative approach could be to always add the .error class on click, but construct your sass such that the properties of .error would only be activated when .disabled is also present.
For example:
<button (click)="isError = true" [ngClass]="{error: isError}">My Button</button>
And then in your SASS:
.disabled.error {
//Your style here
}
If your button is disabled then it won't listen to the (click) event, In this case one thing you can do is to make it look like disabled using css and enable the disabled attribute once its clicked.
in your component:
clickedMe: boolean = false;
In your HTML:
<button [disabled]="(!isTermAgreed || isLoading) && clickedMe" [ngClass]="{'disableCss': (!isTermAgreed || isLoading), 'error': clickedMe && (!isTermAgreed || isLoading)}"(click)="payForStory(paymentObject.StoryId); clickedMe = true">
And you css will be something like:
.disableCss {
background: #dddddd
}
I have an angular (4) based web app, with an #angular/material based card view. Embedded in the content of the card view is a sub-component, which just displays an svg as an <object>.
Here is what that card looks like:
<md-card (click)="onSelect(line)">
<md-card-content (click)="onSelect(line)">
<app-line-overview [line]="line"></app-line-overview>
</md-card-content>
<md-card-footer>
<h2>{{line.name}}</h2>
<h3>OEE: {{line.oee}}</h3>
</md-card-footer>
</md-card>
The issue is that the (click) event doesn't work if I click on the svg image (presumably because it is on top of the card view?), but if I click around the svg, the event fires.
I tried the adding md-card { z-index: 999 } to the css, but it makes no difference. How can I ensure that clicking anywhere within the card fires the event regardless of what is inside it?
Maybe writing a directive with a listener on clicks would help :
#Directive({
selector: '[clickInside]'
})
export class ClickInsideDirective {
constructor(private elementRef: ElementRef) {
}
#Output()
public clickInside = new EventEmitter<Event>();
#HostListener('click', ['$event', '$event.target'])
public onClick(event: MouseEvent, targetElement: HTMLElement): void {
if (!targetElement) {
return;
}
const clickedInside = this.elementRef.nativeElement.contains(targetElement);
if (clickedInside) {
this.clickOutside.emit(event);
}
}
...
}
usage
<md-card (clickInside)="onSelect(line)">