Skip to content

Commit

Permalink
feat: seperate exclude function into own directive to make ng-click-o…
Browse files Browse the repository at this point in the history
…utisde better treeshakable when exclude is not needed (#41)

Co-authored-by: Manuel Schmidt <[email protected]>
  • Loading branch information
Kr0san89 and Manuel Schmidt authored Nov 16, 2023
1 parent 9457f55 commit 24a46ad
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 55 deletions.
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,14 @@ export class AppComponent {

### Options

| Property name | Type | Default | Description |
| ------------- | ---- | ------- | ----------- |
| Property name | Type | Default | Description |
| ------------- | ---- | ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `attachOutsideOnClick` | boolean | `false` | By default, the outside click event handler is automatically attached. Explicitely setting this to `true` sets the handler after the element is clicked. The outside click event handler will then be removed after a click outside has occurred. |
| `clickOutsideEnabled` | boolean | `true` | Enables directive. |
| `clickOutsideEvents` | string | `'click'` | A comma-separated list of events to cause the trigger. For example, for additional mobile support: `[clickOutsideEvents]="'click,touchstart'"`. |
| `delayClickOutsideInit` | boolean | `false` | Delays the initialization of the click outside handler. This may help for items that are conditionally shown ([see issue #13](https://github.com/arkon/ng-click-outside/issues/13)). |
| `emitOnBlur` | boolean | `false` | If enabled, emits an event when user clicks outside of applications' window while it's visible. Especially useful if page contains iframes. |
| `exclude` | string | | A comma-separated string of DOM element queries to exclude when clicking outside of the element. For example: `[exclude]="'button,.btn-primary'"`. |
| `excludeBeforeClick` | boolean | `false` | By default, `clickOutside` registers excluded DOM elements on init. This property refreshes the list before the `clickOutside` event is triggered. This is useful for ensuring that excluded elements added to the DOM after init are excluded (e.g. ng2-bootstrap popover: this allows for clicking inside the `.popover-content` area if specified in `exclude`). |
| `clickOutsideEnabled` | boolean | `true` | Enables directive. |
| `clickOutsideEvents` | string | `'click'` | A comma-separated list of events to cause the trigger. For example, for additional mobile support: `[clickOutsideEvents]="'click,touchstart'"`. |
| `delayClickOutsideInit` | boolean | `false` | Delays the initialization of the click outside handler. This may help for items that are conditionally shown ([see issue #13](https://github.com/arkon/ng-click-outside/issues/13)). |
| `emitOnBlur` | boolean | `false` | If enabled, emits an event when user clicks outside of applications' window while it's visible. Especially useful if page contains iframes. |
| `clickOutsideExclude` | string | | A comma-separated string of DOM element queries to exclude when clicking outside of the element. (Import NgClickOutsideExcludeDirective) For example: `[clickOutsideExclude]="'button,.btn-primary'"`. |

## Migration from ng-click-outside

Expand Down
2 changes: 2 additions & 0 deletions projects/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
(clickOutside)="onClickedOutside($event)"
[attachOutsideOnClick]="attachOutsideOnClick"
[clickOutsideEnabled]="enabled"
[clickOutsideExclude]="'.foo'"
[emitOnBlur]="true">
<p>Clicked inside: {{countInside}}</p>
<p>Clicked outside: {{countOutside}}</p>
Expand All @@ -17,3 +18,4 @@
<span>Enabled</span>
</label>
</div>
<div class="foo" [style.height.px]="200"></div>
5 changes: 3 additions & 2 deletions projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import {NgClickOutsideDirective} from "ng-click-outside2";
import {NgClickOutsideDirective, NgClickOutsideExcludeDirective} from "ng-click-outside2";

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({ appId: 'serverApp' }),
NgClickOutsideDirective
NgClickOutsideDirective,
NgClickOutsideExcludeDirective
],
providers: [],
bootstrap: [AppComponent]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { NgClickOutsideExcludeDirective } from './ng-click-outside-exclude.directive';
import {Component, ViewChild} from "@angular/core";
import {NgClickOutsideDirective} from "./ng-click-outside.directive";
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {DOCUMENT} from "@angular/common";
import {By} from "@angular/platform-browser";


@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'test-click',
standalone: true,
imports: [NgClickOutsideDirective, NgClickOutsideExcludeDirective],
template: `
<button id="b-1" (click)="clickButton1 = clickButton1 + 1"></button>
<button id="b-2" (clickOutside)="clickOutsideButton2 = clickOutsideButton2 + 1" [clickOutsideExclude]="'.no-outside-click'"
(click)="clickButton2 = clickButton2 + 1" [clickOutsideEnabled]="enabled"
></button>
<button id="b-3" (click)="clickButton3 = clickButton3 + 1" class="no-outside-click"></button>
`
})
class TestClickOutsideComponent {
@ViewChild(NgClickOutsideDirective) ngClickOutsideDirective?: NgClickOutsideDirective
clickOutsideButton2 = 0;
clickButton1 = 0;
clickButton2 = 0;
clickButton3 = 0;
enabled = true
}

describe('NgClickOutsideExcludeDirective', () => {
let component: TestClickOutsideComponent;
let fixture: ComponentFixture<TestClickOutsideComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TestClickOutsideComponent],
providers: [{provider: DOCUMENT, useValue: document}]
}).compileComponents();

fixture = TestBed.createComponent(TestClickOutsideComponent);
component = fixture.componentInstance;
fixture.detectChanges();
})


it('should excludeDirective should be defined when attribute exclude exists ', () => {
expect(component.ngClickOutsideDirective!.excludeDirective).toBeDefined();
});

it('should not count click of Button 3 as it is excluded', () => {
const button3 = fixture.debugElement.query(By.css('#b-3'));
button3.nativeElement.click();
expect(component.clickButton3).toBe(1);
expect(component.clickButton2).toBe(0);
expect(component.clickOutsideButton2).toBe(0); });

});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {Directive, inject, Inject, Input} from '@angular/core';
import {DOCUMENT} from "@angular/common";

/**
* Optimization for Treeshaking: https://angular.io/guide/lightweight-injection-tokens
*/
export abstract class NgClickOutsideExcludeToken {
abstract isExclude(target: any): boolean;
}

@Directive({
selector: '[clickOutsideExclude]',
standalone: true,
providers: [
{provide: NgClickOutsideExcludeToken, useExisting: NgClickOutsideExcludeDirective}
]
})
export class NgClickOutsideExcludeDirective extends NgClickOutsideExcludeToken {

/**
* A comma-separated string of DOM element queries to exclude when clicking outside of the element.
* For example: `[exclude]="'button,.btn-primary'"`.
*/
@Input() clickOutsideExclude = '';

private document: Document = inject(DOCUMENT);

public excludeCheck(): HTMLElement[] {
if (this.clickOutsideExclude) {
try {
const nodes = Array.from(this.document.querySelectorAll(this.clickOutsideExclude)) as Array<HTMLElement>;
if (nodes) {
return nodes;
}
} catch (err) {
console.error('[ng-click-outside] Check your exclude selector syntax.', err);
}
}
return [];
}

public isExclude(target: any): boolean {
const nodesExcluded = this.excludeCheck();
for (let excludedNode of nodesExcluded) {
if (excludedNode.contains(target)) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {NgClickOutsideDirective} from "./ng-click-outside.directive";
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {Component} from "@angular/core";
import {Component, ViewChild} from "@angular/core";
import {By} from "@angular/platform-browser";
import {DOCUMENT} from "@angular/common";

Expand All @@ -16,6 +16,7 @@ import {DOCUMENT} from "@angular/common";
></button>`
})
class TestClickOutsideComponent {
@ViewChild(NgClickOutsideDirective) ngClickOutsideDirective?: NgClickOutsideDirective
clickOutsideButton2 = 0;
clickButton1 = 0;
clickButton2 = 0;
Expand Down Expand Up @@ -52,4 +53,8 @@ describe('NgClickOutsideDirective', () => {
expect(component.clickButton2).toBe(1);
expect(component.clickOutsideButton2).toBe(0);
});

it('should excludeDirective not exist if we do not define exclude ', () => {
expect(component.ngClickOutsideDirective!.excludeDirective).toBeNull();
});
});
49 changes: 5 additions & 44 deletions projects/ng-click-outside2/src/lib/ng-click-outside.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Directive,
ElementRef,
EventEmitter,
inject,
Inject,
Input,
NgZone,
Expand All @@ -12,6 +13,7 @@ import {
SimpleChanges,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {NgClickOutsideExcludeToken} from "./ng-click-outside-exclude.directive";

/**
* Directove to detect clicks outside of the current element
Expand Down Expand Up @@ -59,20 +61,6 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
*/
@Input() emitOnBlur = false;

/**
* A comma-separated string of DOM element queries to exclude when clicking outside of the element.
* For example: `[exclude]="'button,.btn-primary'"`.
*/
@Input() exclude = '';
/**
* By default, `clickOutside` registers excluded DOM elements on init.
*
* This property refreshes the list before the `clickOutside` event is triggered. This is useful for ensuring that
* excluded elements added to the DOM after init are excluded (e.g. ng2-bootstrap popover: this allows for clicking
* inside the `.popover-content` area if specified in `exclude`).
*/
@Input() excludeBeforeClick = false;

/**
* A comma-separated list of events to cause the trigger.
* ### For example, for additional mobile support:
Expand All @@ -85,7 +73,7 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
*/
@Output() clickOutside: EventEmitter<Event> = new EventEmitter<Event>();

private _nodesExcluded: Array<HTMLElement> = [];
excludeDirective = inject(NgClickOutsideExcludeToken, {host: true, optional: true});
private _events: Array<string> = ['click'];

constructor(
Expand All @@ -108,7 +96,7 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
}

ngOnChanges(changes: SimpleChanges) {
if (changes['attachOutsideOnClick'] || changes['exclude'] || changes['emitOnBlur']) {
if (changes['attachOutsideOnClick'] || changes['emitOnBlur']) {
this._init();
}
}
Expand All @@ -118,8 +106,6 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
this._events = this.clickOutsideEvents.split(',').map(e => e.trim());
}

this._excludeCheck();

if (this.attachOutsideOnClick) {
this._initAttachOutsideOnClickListener();
} else {
Expand All @@ -139,29 +125,13 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
}
}

private _excludeCheck() {
if (this.exclude) {
try {
const nodes = Array.from(this.document.querySelectorAll(this.exclude)) as Array<HTMLElement>;
if (nodes) {
this._nodesExcluded = nodes;
}
} catch (err) {
console.error('[ng-click-outside] Check your exclude selector syntax.', err);
}
}
}

private _onClickBody(ev: Event) {
if (!this.clickOutsideEnabled) {
return;
}

if (this.excludeBeforeClick) {
this._excludeCheck();
}

if (!this._el.nativeElement.contains(ev.target) && !this._shouldExclude(ev.target)) {
if (!this._el.nativeElement.contains(ev.target) && !this.excludeDirective?.isExclude(ev.target)) {
this._emit(ev);

if (this.attachOutsideOnClick) {
Expand Down Expand Up @@ -190,15 +160,6 @@ export class NgClickOutsideDirective implements OnInit, OnChanges, OnDestroy {
this._ngZone.run(() => this.clickOutside.emit(ev));
}

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

return false;
}

private _initClickOutsideListener() {
this._ngZone.runOutsideAngular(() => {
Expand Down
1 change: 1 addition & 0 deletions projects/ng-click-outside2/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*/

export * from './lib/ng-click-outside.directive';
export * from './lib/ng-click-outside-exclude.directive';

0 comments on commit 24a46ad

Please sign in to comment.