Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat(template): unpatch all but specified events #1733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
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
11 changes: 10 additions & 1 deletion 11 apps/docs/docs/template/api/unpatch-directive.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,22 @@ The `unpatch` directive solves this problem in a convenient way:
> 1. Elements that should trigger navigation (with `routerLink` directly or with method bound to `(click)` or other events). Otherwise you will end up having a 'Navigation triggered outside Angular zone, did you forget to call "ngZone.run()"?' warning.
> 2. Elements that reference a `FormControl`. Specify all events except the `blur` and `change` events. Otherwise user input is ignored, the `FormControl.valueChanges` observable will not emit and attached validations to the FormControl will not run until next change detection that affects the component in which the element is rendered.

Included Features:
## Included Features:

- by default un-patch all registered listeners of the host it is applied on
- un-patch only a specified set of registered event listeners
- un-patch all events listeners except a specified set
- works zone independent (it directly checks the window for patched APIs and un-patches them without the use of `runOutsideZone` which brings more performance)
- Not interfering with any logic executed by the registered callback

## Apply in three distinct ways:

1. Unpatch all events: `<div [unpatch]>...<div>`
2. Unpatch specified events\*: `<div [unpatch]="['mouseenter', 'mouseleave']">...<div>`
3. Unpatch all except specified events\*: `<div [unpatch]="['!mouseenter', '!mouseleave']">...<div>`

When combining negated and non-negated events i.e: `<div [unpatch]="['!mouseenter', 'mouseleave']">...<div>` all non-negated events are ignored and an error will be thrown in dev mode!

## Current list of unpatched events

```typescript
Expand Down
45 changes: 41 additions & 4 deletions 45 libs/template/unpatch/src/lib/tests/unpatch.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ describe(RxUnpatch.name, () => {
const div = fixture.debugElement.query(By.css('div'));
const addEventListener = jest.spyOn(
div.nativeElement,
Zone.__symbol__('addEventListener')
Zone.__symbol__('addEventListener'),
);
const removeEventListener = jest.spyOn(
div.nativeElement,
'removeEventListener'
'removeEventListener',
);

// Act
Expand Down Expand Up @@ -87,11 +87,11 @@ describe(RxUnpatch.name, () => {
const div = fixture.debugElement.query(By.css('div'));
const addEventListener = jest.spyOn(
div.nativeElement,
Zone.__symbol__('addEventListener')
Zone.__symbol__('addEventListener'),
);
const removeEventListener = jest.spyOn(
div.nativeElement,
'removeEventListener'
'removeEventListener',
);

// Act
Expand All @@ -115,4 +115,41 @@ describe(RxUnpatch.name, () => {
addEventListener.mockRestore();
}
});

it('should re-apply all except provided negated event listeners', () => {
// Arrange
const fixture = TestBed.createComponent(TestComponent);
fixture.componentInstance.unpatch = ['!mouseenter'];
const appRef = TestBed.inject(ApplicationRef);
const div = fixture.debugElement.query(By.css('div'));
const addEventListener = jest.spyOn(
div.nativeElement,
Zone.__symbol__('addEventListener'),
);
const removeEventListener = jest.spyOn(
div.nativeElement,
'removeEventListener',
);

// Act
fixture.detectChanges();
const tick = jest.spyOn(appRef, 'tick');
div.nativeElement.dispatchEvent(new Event('mouseenter'));
div.nativeElement.dispatchEvent(new Event('click'));

try {
// Assert
expect(logs).toEqual([
[LogEvent.Mouseenter, true],
[LogEvent.Click, false],
]);
// Change detection has been run once since we unpatched only `!mouseenter`.
expect(tick).toHaveBeenCalledTimes(1);
expect(addEventListener).toHaveBeenCalledTimes(1);
expect(removeEventListener).toHaveBeenCalledTimes(1);
} finally {
tick.mockRestore();
addEventListener.mockRestore();
}
});
});
53 changes: 50 additions & 3 deletions 53 libs/template/unpatch/src/lib/unpatch.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function unpatchEventListener(
element: HTMLElement & {
eventListeners?: (event: string) => EventListenerOrEventListenerObject[];
},
event: string
event: string,
): EventListenerOrEventListenerObject[] {
// `EventTarget` is patched only in the browser environment, thus
// running this code on the server-side will throw an exception:
Expand All @@ -61,7 +61,7 @@ export function unpatchEventListener(

const addEventListener = getZoneUnPatchedApi(
element,
'addEventListener'
'addEventListener',
).bind(element) as typeof element.addEventListener;

const listeners: EventListenerOrEventListenerObject[] = [];
Expand All @@ -77,6 +77,10 @@ export function unpatchEventListener(
return listeners;
}

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || !!ngDevMode;

/**
* @Directive RxUnpatch
*
Expand All @@ -103,9 +107,19 @@ export function unpatchEventListener(
* Included Features:
* - by default un-patch all registered listeners of the host it is applied on
* - un-patch only a specified set of registered event listeners
* - un-patch all events listeners except a specified set
* - works zone independent (it directly checks the widow for patched APIs and un-patches them without the use of `runOutsideZone` which brings more performance)
* - Not interfering with any logic executed by the registered callback
*
* Apply in three distinct ways:
*
* 1. Unpatch all events: `<div [unpatch]>...<div>`
* 2. Unpatch specified events*: `<div [unpatch]="['mouseenter', 'mouseleave']">...<div>`
* 3. Unpatch all except specified events*: `<div [unpatch]="['!mouseenter', '!mouseleave']">...<div>`
*
* When combining negated and non-negated events i.e: `<div [unpatch]="['!mouseenter', 'mouseleave']">...<div>` all non-negated events are ignored and an error will be thrown in dev mode!
*
*
* @usageNotes
*
* The `unpatch` directive can be used like shown here:
Expand Down Expand Up @@ -137,7 +151,21 @@ export class RxUnpatch implements OnChanges, AfterViewInit, OnDestroy {

ngOnChanges({ events }: SimpleChanges): void {
if (events && Array.isArray(this.events)) {
this.events$.next(this.events);
const negatedEvents = this.events
.filter((event) => event.startsWith('!'))
.map((event) => event.replace('!', ''));

const nextEvents = negatedEvents.length
? zonePatchedEvents.filter(
(zonePatchedEvent) => !negatedEvents.includes(zonePatchedEvent),
)
: this.events;

if (NG_DEV_MODE) {
this.validateEvents(nextEvents, negatedEvents);
}

this.events$.next(nextEvents);
}
}

Expand All @@ -163,4 +191,23 @@ export class RxUnpatch implements OnChanges, AfterViewInit, OnDestroy {
this.listeners.set(event, listeners);
}
}

private validateEvents(nextEvents: string[], negatedEvents: string[]): void {
// check if user has specified negated and non-negated events
if (negatedEvents.length && negatedEvents.length !== this.events.length) {
throw new Error(
`Invalid value [${this.events.toString()}] specified for unpatch directive! Cannot combine negated & non-negated events!`,
);
}

// check if user has specified invalid events
const unknownEvents = [...negatedEvents, ...nextEvents].filter(
(event) => !zonePatchedEvents.includes(event),
);
if (unknownEvents.length) {
throw new Error(
`Unknown events [${unknownEvents.toString()}] specified for unpatch directive!`,
);
}
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.