Skip to content
This repository has been archived by the owner on Nov 16, 2021. It is now read-only.

Various tweaks #28

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 42 additions & 20 deletions src/click-outside.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import {
PLATFORM_ID,
SimpleChanges,
NgZone,
Renderer2,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { merge } from 'rxjs/observable/merge';
import { takeUntil } from 'rxjs/operators/takeUntil';

@Injectable()
@Directive({ selector: '[clickOutside]' })
Expand All @@ -23,55 +28,62 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
@Input() delayClickOutsideInit: boolean = false;
@Input() exclude: string = '';
@Input() excludeBeforeClick: boolean = false;
@Input() clickOutsideEvents: string = '';
@Input() set clickOutsideEvents(events: string) {
this._events = events.split(',').map(e => e.trim());
}
@Input() clickOutsideEnabled: boolean = true;

@Output() clickOutside: EventEmitter<Event> = new EventEmitter<Event>();

private _nodesExcluded: Array<HTMLElement> = [];
private _events: Array<string> = ['click'];
private _isPlatformBrowser: boolean = isPlatformBrowser(this.platformId);

private _beforeInit: Subject<void> = new Subject<void>();
private _onDestroy: Subject<void> = new Subject<void>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of _beforeInit and _onDestroy, exactly?

private _onOutsideClick: Subject<void> = new Subject<void>();

constructor(private _el: ElementRef,
private _renderer2: Renderer2,
private _ngZone: NgZone,
@Inject(PLATFORM_ID) private platformId: Object) {
this._initOnClickBody = this._initOnClickBody.bind(this);
this._onClickBody = this._onClickBody.bind(this);
}

ngOnInit() {
if (!isPlatformBrowser(this.platformId)) { return; }
if (!this._isPlatformBrowser) { return; }

this._init();
}

ngOnDestroy() {
if (!isPlatformBrowser(this.platformId)) { return; }
if (!this._isPlatformBrowser) { return; }

if (this.attachOutsideOnClick) {
this._events.forEach(e => this._el.nativeElement.removeEventListener(e, this._initOnClickBody));
}
this._onDestroy.next();
this._onDestroy.complete();

this._events.forEach(e => document.body.removeEventListener(e, this._onClickBody));
this._onOutsideClick.complete();
this._beforeInit.complete();
}

ngOnChanges(changes: SimpleChanges) {
if (!isPlatformBrowser(this.platformId)) { return; }
if (!this._isPlatformBrowser) { return; }

if (changes['attachOutsideOnClick'] || changes['exclude']) {
this._init();
}
}

private _init() {
if (this.clickOutsideEvents !== '') {
this._events = this.clickOutsideEvents.split(',').map(e => e.trim());
}
this._beforeInit.next();

this._excludeCheck();

if (this.attachOutsideOnClick) {
this._ngZone.runOutsideAngular(() => {
this._events.forEach(e => this._el.nativeElement.addEventListener(e, this._initOnClickBody));
this._listenAll(this._el.nativeElement, ...this._events)
.subscribe(this._initOnClickBody);
});
} else {
this._initOnClickBody();
Expand All @@ -88,7 +100,11 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy {

private _initClickListeners() {
this._ngZone.runOutsideAngular(() => {
this._events.forEach(e => document.body.addEventListener(e, this._onClickBody));
this._listenAll('body', ...this._events)
.pipe(
takeUntil(this._onOutsideClick),
)
.subscribe(this._onClickBody);
});
}

Expand Down Expand Up @@ -118,18 +134,24 @@ export class ClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
this._ngZone.run(() => this.clickOutside.emit(ev));

if (this.attachOutsideOnClick) {
this._events.forEach(e => document.body.removeEventListener(e, this._onClickBody));
this._onOutsideClick.next();
}
}
}

private _shouldExclude(target): boolean {
for (let excludedNode of this._nodesExcluded) {
if (excludedNode.contains(target)) {
return true;
}
}
return this._nodesExcluded.some(excludedNode => excludedNode.contains(target));
}

private _listenAll(target: 'window' | 'document' | 'body' | any, ...eventNames: string[]): Observable<Event> {
const sources = eventNames.map(eventName => {
return new Observable<Event>(observer => this._renderer2.listen(target, eventName, ev => observer.next(ev)));
});

return false;
return merge(...sources)
.pipe(
takeUntil(this._beforeInit),
takeUntil(this._onDestroy),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty unfamiliar with some rxjs practices. Could you explain what's going on here?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this link will answer both your questions:
http://brianflove.com/2016/12/11/anguar-2-unsubscribe-observables/
takeUntil() is a practice to make sure a subscribe gets unsubscribed for sure when some action occurs.
the action can be defined with Subject, and then when you call Subject.next() Subject.complete() (isoden added this in lines 63-64 of this pull request) you make the action happens, which in turn makes the unsubscribe happen for the subscribe that was registered to this Subject (with takeUntil).

Copy link

@ndcunningham ndcunningham Apr 15, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't you just add a takeUntil(merge(this._beforeInit, this._onDestroy))
instead of double takeUntils ?

);
}
}