Skip to content

Instantly share code, notes, and snippets.

@cbejensen
Last active September 24, 2020 16:30
Show Gist options
  • Save cbejensen/00db1f64ba1c6caa12ea807ad8ac4717 to your computer and use it in GitHub Desktop.
Save cbejensen/00db1f64ba1c6caa12ea807ad8ac4717 to your computer and use it in GitHub Desktop.
Angular scan directive
import { Directive, EventEmitter, HostListener, Input, OnInit, OnDestroy, Optional, Output, Self } from '@angular/core';
import { NgControl } from '@angular/forms';
import { Subject } from 'rxjs';
import { debounceTime, filter, map, pairwise, startWith, takeUntil } from 'rxjs/operators';
/**
* Detects when an input is coming from a scanner (or being pasted).
*/
@Directive({
selector: '[appScan]',
})
export class ScanDirective implements OnInit, OnDestroy {
/**
* The minimum amount of character additions within the set `debounceTime` for a change to be considered as coming
* from a scanner (or paste).
*/
// tslint:disable-next-line:no-input-rename
@Input('appScanThreshold') threshold = 10;
/**
* How long to wait between each input change before checking the amount of characters entered. If many characters are
* entered in a short amount of time, it's probably a scanner (or was pasted).
*/
// tslint:disable-next-line:no-input-rename
@Input('appScanDebounceTime') debounceTime = 100;
/**
* Emits the value of an input change that appears to be from a scanner.
*/
@Output() scan = new EventEmitter();
private _destroyed = new Subject();
private _valueChanges = new Subject<string>();
constructor(@Self() @Optional() public ngControl?: NgControl) {
super();
}
ngOnInit(): void {
const control = this.ngControl?.control;
// If there's an Angular form control, subscribe to its value stream. Otherwise, assume the host element is a
// native form element and subscribe to our own Observable that will emit when the host element emits an `input`
// event.
(control?.valueChanges || this._valueChanges)
.pipe(
debounceTime(this.debounceTime),
// Use `startWith` so the first value gets through `pairwise`, which requires 2 values before emitting.
startWith(''),
pairwise(),
// Only consider it a valid scan/paste if the input was empty before this and the new value's char count
// exceeds the threshold.
filter(([oldVal, newVal]) => !oldVal && newVal?.length >= this.threshold),
map(([oldVal, newVal]) => newVal),
takeUntil(this._destroyed)
)
.subscribe(val => this.scan.emit(val));
}
ngOnDestroy() {
this._destroyed.next();
this._destroyed.complete();
}
/**
* Only effective if this directive is on an element that is not attached to an Angular form control.
*/
@HostListener('input', ['$event'])
setValue = (e: Event) => {
const { value } = e.target as HTMLInputElement;
this._valueChanges.next(value);
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment