Issue
I have the following use case:
There is an input
and a popup should be displayed next to the input whenever it has focus. So while the input
has focus, the user can either type into the input
or use the popup to select something.
When the user is done, he can unfocus the element either by clicking
somewhere else or by pressing tab and focussing the next element.
However, if he clicks a button inside the popup, the popup should stay
open and the input should stay focused so that the user can continue
typing.
The problem that I have is that angular processes (blur) on the input before it processes (click)
in the popup. That means that when the user
clicks in the popup, the input loses focus, it gets hidden, and then the
click of the user won’t be processed anymore.
I have made a stackblitz-demo for the problem:
This is the source code:
app.component.html
<h1>Hello</h1>
<p>
Type 'one', 'two' or 'three' to change number or select a number
from popup.
<br>
<input type="checkbox" [(ngModel)]="hideHelperOnBlur" /> Hide
helper on blur (if this is set, then the buttons in the popup don't work
but if this is not set, the popup doesn't close if the input looses
focus -> how can I get both? The buttons to work but the popup still
closing on blur?)
</p>
Your number: {{selectedNumber}}
<p>
Change number:
<input [ngModel]="formattedNumber" (ngModelChange)="newNumberTyped($event)" (focus)="helperVisible = true"
(blur)="processBlur()" #mainInput />
<div *ngIf="helperVisible">
<button *ngFor="let number of numbers" (click)="selectNumber(number.raw)">{{number.formatted}} </button>
</div>
app.component.ts
import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
name = 'Angular';
formattedNumber: string = 'three';
selectedNumber: number = 3;
helperVisible: boolean = false;
numbers: any[] = [
{ raw: 1, formatted: 'one' },
{ raw: 2, formatted: 'two' },
{ raw: 3, formatted: 'three' }
];
@ViewChild('mainInput', { static: false })
mainInput: ElementRef;
hideHelperOnBlur: boolean;
newNumberTyped(newFormattedNumber: string) {
this.numbers.forEach((number) => {
if (newFormattedNumber == number.formatted) {
this.formattedNumber = newFormattedNumber;
this.selectedNumber = number.raw;
}
});
}
selectNumber(newRawNumber: number) {
this.numbers.forEach((number) => {
if (newRawNumber == number.raw) {
this.formattedNumber = number.formatted;
this.selectedNumber = newRawNumber;
}
});
this.mainInput.nativeElement.focus();
}
processBlur() {
if (this.hideHelperOnBlur) {
this.helperVisible = false;
}
}
}
I expect the following behavior:
- When the input gets focus the popup is visible
- When the user clicks outside anywhere on the page that is not the
popup or the input, the popup closes - When the user presses the tab while the input is focussed, the input
loses focus and the popup closes - When the user clicks a button in the popup, that number is selected
I only seem to be able to get either the second and third or the fourth
criteria to work (see the checkbox in the demo).
What I already tried:
- Using @HostListener with focusin, focusout or focus or
(blur)="someFunction(event) to find out which element has the new focus
to check whether that element is in the popup or the input -> this
doesn’t seem to work since at the time of focusout-event it is not yet
clear who gets the new focus - using a timeout so that angular already finished processing the
click event when the timeout is over -> this seems to work but working
with timeouts is a really clunky solution that may break easily
Does anyone have a solution?
Solution
This is more of a Javascript thing. The blur
event takes place before the click
event. The click
event only takes place once the mouse button is released.
You can use the mousedown
event here to your advantage. The mousedown
event takes place before the blur
event. Simply call preventDefault
on mousedown
in the popover buttons to prevent the input
from losing focus. This would also solve the issue where your input
blinks when you click buttons in the popover so you can get rid of this.mainInput.nativeElement.focus();
in your selectNumber
function.
<button *ngFor="let number of numbers" (mousedown)="$event.preventDefault()" (click)="selectNumber(number.raw)">{{number.formatted}}</button>
Here is a working example on StackBlitz.