Skip to content

Commit

Permalink
feat: add click outside directive (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dale Nguyen authored Oct 21, 2023
1 parent 6c2de00 commit f187eed
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 0 deletions.
33 changes: 33 additions & 0 deletions docs/src/content/docs/utilities/Directives/click-outside.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: clickOutside
description: An Angular directive that is used to detect clicks outside the element.
---

## Import

```ts
import { ClickOutside } from 'ngxtension/click-outside';
```

## Usage

### Basic

Add `clickOutside` directive directly to the Angular element.

```ts
@Component({
standalone: true,
template: `
<div (clickOutside)="close()"></div>
`,
imports: [ClickOutside],
})
class TestComponent {
close() {
// close logic
}
}
```

This will trigger the `close()` method when user clicks outside of the target element.
3 changes: 3 additions & 0 deletions libs/ngxtension/click-outside/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/click-outside

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/click-outside`.
5 changes: 5 additions & 0 deletions libs/ngxtension/click-outside/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/click-outside/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/click-outside",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/click-outside/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["click-outside"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/click-outside/**/*.ts",
"libs/ngxtension/click-outside/**/*.html"
]
}
}
}
}
44 changes: 44 additions & 0 deletions libs/ngxtension/click-outside/src/click-outside.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Component, DebugElement, ElementRef } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { ClickOutside } from './click-outside';

@Component({
standalone: true,
template: `
<div clickOutside></div>
`,
imports: [ClickOutside],
})
class TestComponent {
constructor(public elementRef: ElementRef) {}
}

describe('ClickOutside', () => {
let fixture: ComponentFixture<TestComponent>;
let directive: ClickOutside;
let debugElement: DebugElement;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ClickOutside, TestComponent],
});

fixture = TestBed.createComponent(TestComponent);
debugElement = fixture.debugElement.query(By.directive(ClickOutside));
directive = debugElement.injector.get(ClickOutside);

fixture.detectChanges();
});

it('should create the directive', () => {
expect(directive).toBeTruthy();
});

it('should emit clickOutside event when a click occurs outside the element', () => {
jest.spyOn(directive.clickOutside, 'emit');
const fakeEvent = new MouseEvent('click');
document.body.click();
expect(directive.clickOutside.emit).toHaveBeenCalledWith(fakeEvent);
});
});
65 changes: 65 additions & 0 deletions libs/ngxtension/click-outside/src/click-outside.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { DOCUMENT } from '@angular/common';
import {
Directive,
ElementRef,
EventEmitter,
inject,
NgZone,
Output,
} from '@angular/core';

import type { OnInit } from '@angular/core';
import { createInjectionToken } from 'ngxtension/create-injection-token';
import { injectDestroy } from 'ngxtension/inject-destroy';
import { filter, fromEvent, Subject, takeUntil } from 'rxjs';

/*
* This function is used to detect clicks in the document.
* It is used by the clickOutside directive.
*/
const [injectDocumentClick] = createInjectionToken(() => {
const click$ = new Subject<Event>();
const [ngZone, document] = [inject(NgZone), inject(DOCUMENT)];

ngZone.runOutsideAngular(() => {
fromEvent(document, 'click').subscribe(click$);
});

return click$;
});

/*
* This directive is used to detect clicks outside the element.
*
* Example:
* <div (clickOutside)="close()"></div>
*
*/
@Directive({ selector: '[clickOutside]', standalone: true })
export class ClickOutside implements OnInit {
private ngZone = inject(NgZone);
private elementRef = inject(ElementRef);
private documentClick$ = injectDocumentClick();

private destroy$ = injectDestroy();

/*
* This event is emitted when a click occurs outside the element.
*/
@Output() clickOutside = new EventEmitter<Event>();

ngOnInit() {
this.documentClick$
.pipe(
takeUntil(this.destroy$),
filter(
(event: Event) =>
!this.elementRef.nativeElement.contains(event.target)
)
)

.subscribe((event: Event) => {
this.ngZone.run(() => this.clickOutside.emit(event));
});
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/click-outside/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './click-outside';
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
"libs/ngxtension/assert-injector/src/index.ts"
],
"ngxtension/call-apply": ["libs/ngxtension/call-apply/src/index.ts"],
"ngxtension/click-outside": [
"libs/ngxtension/click-outside/src/index.ts"
],
"ngxtension/computed-from": [
"libs/ngxtension/computed-from/src/index.ts"
],
Expand Down

0 comments on commit f187eed

Please sign in to comment.