17

In Angular2 how to know when ANY input field has lost focus..! If I use observables on the form:

form.valueChange.subscribe...

wont work since I really want to know when a field lost it's blur (focus) so I can update my store (if I update the store before losing focus, my cursor on a text input gets moved to the end, since the data gets swapped which is weird looking)

of course I can also add (change)="" on each input, but I have a lot of'em...

I was thinking something of the sorts of:

this.form.valueChanges.debounceTime(1000).subscribe((changes:any) => {
  if (this.form.dirty){
    this.appStore.dispatch(this.resellerAction.updateResellerInfo(changes))
  }
});

but the problem is that the dirty remains dirty, so it stuck in an everlasting loop of change detections...

tx

Sean

born2net
  • 24,129
  • 22
  • 65
  • 104

2 Answers2

28

The blur event doesn't bubble, therefore we need to listen on every input element directly. Angular provides a nice solution for this situation.

A directive that applies to all input elements inside your template.

This directive uses a host-listener to listen for the blur events on all elements where the selector applies and forwards a bubbling input-blur event:

@Directive({
  selector: 'input,select',
  host: {'(blur)': 'onBlur($event)'}
})
class BlurForwarder {
  constructor(private elRef:ElementRef, private renderer:Renderer) {}

  onBlur($event) {
    this.renderer.invokeElementMethod(this.elRef.nativeElement, 
        'dispatchEvent', 
        [new CustomEvent('input-blur', { bubbles: true })]);
    // or just 
    // el.dispatchEvent(new CustomEvent('input-blur', { bubbles: true }));
    // if you don't care about webworker compatibility
  }
}

By adding the BlurForwarder directive to directives: [...] it will be applied to all elements in its template that match the selector.
The host-listener listens for bubbling input-blur events and calls our event handler:

@Component({
  selector: 'my-component',
  directives: [BlurForwarder],
  host: {'(input-blur)':'onInputBlur($event)'},
  template: `
<form>
  <input type="text" [(ngModel)]="xxx">
  <input type="text" [(ngModel)]="yyy">
  <input type="text" [(ngModel)]="zzz">
</form>`
}) {
  onInputBlur(event) {
    doSomething();
  }
}
André
  • 12,497
  • 6
  • 42
  • 44
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    SWEEEEET.... just tested and it worked GREAT!!! u d man Gunter!!!! I was able to remove all my (change)="onChange($event)" from the form – born2net Apr 01 '16 at 17:58
  • @Günter Zöchbauer, do you have an alternative solution for rc6? I believe directives are deprecated in that version. Thanks! – PBandJ Feb 07 '17 at 22:11
  • 1
    just move `directives` to `declarations in `@NgModule(...)` – Günter Zöchbauer Feb 08 '17 at 06:35
  • 1
    this is exactly what I've been looking for – tylerlindell Apr 12 '17 at 17:30
  • @GünterZöchbauer, I've been reading up on HostBinding / HostListener and it seems like the style guide recommends we utilize these over the "host" property on the component decorator. Is this possible, and if so, could you provide an example? All the examples seem to only show setting these attributes on the directive itself. Thanks! – cjones26 Aug 15 '17 at 19:44
  • `@HostListener` is just different syntax for the same thing – Günter Zöchbauer Aug 15 '17 at 19:57
  • 1
    Thank you, you've saved my day :) – David Votrubec Feb 06 '18 at 16:26
  • Glad to hear that worked for you. The stackblitz reproduction was very helpful. – Günter Zöchbauer Feb 06 '18 at 16:27
  • _The answer about `focusout` is superior and should be used instead._ focusout is supported by all browsers including IE going back to IE 8. This spins up a component and listeners for every single input/select, and won't work for any other custom controls you have in the future. You could certainly retrofit the code above to use it, but focusout is what you should be using, its much, much, much more efficient. – dudewad Sep 07 '18 at 15:54
13

Why not use focusout it bubbles by default in the DOM

Here's a simple directive that catches focusout and checks if input value is blank, then sets value to zero:

@Directive({
  selector: '[blankToZero]'
})
export class BlankToZeroDirective {
  constructor(private elementHost: ElementRef) { }

  @HostListener('focusout')
  ensureInput(): void {
    if (Util.isNullOrEmpty(this.elementHost.nativeElement.value)) {
      this.elementHost.nativeElement.value = 0;
    }
  }
}
Kon
  • 27,113
  • 11
  • 60
  • 86
TrickySituation
  • 378
  • 4
  • 10
  • 3
    Best solution, simple and native – TetraDev Feb 23 '18 at 20:53
  • 3
    why not elaborate on your answer a little bit more, maybe add an example? – Choco May 21 '18 at 05:18
  • 1
    This is the best solution, and I would have posted it myself if it weren't already here. The accepted answer spins up an entire component and listeners for every single form field in the DOM whereby focusout could be used at the root of the form, one and done. _I repeat, this is a superior answer!!_ – dudewad Sep 07 '18 at 15:53
  • No there could be situations where you want to have this functionality selectively and hence accepted answer is better. e.g. in our case we want to do analytics call on certain elements only when user burs out with single focusout on form its too generic and doesnt help to apply the solution selectively. – Shailesh Vaishampayan Oct 17 '18 at 11:38
  • In year 2023 this does not work any longer for Angular 15 – Paul Schwartzberg Jul 06 '23 at 21:10