refactor(core): Remove global event delegation code. (#57893)

This is no longer needed since we are no longer experimenting with it.

PR Close #57893
This commit is contained in:
Thomas Nguyen 2024-09-19 15:50:40 -07:00 committed by Alex Rickabaugh
parent e40a4fa3c7
commit 2e7cfcb2ef
19 changed files with 5 additions and 588 deletions

View file

@ -72,11 +72,7 @@ export {
withI18nSupport as ɵwithI18nSupport,
} from './hydration/api';
export {withEventReplay as ɵwithEventReplay} from './hydration/event_replay';
export {
GLOBAL_EVENT_DELEGATION as ɵGLOBAL_EVENT_DELEGATION,
JSACTION_EVENT_CONTRACT as ɵJSACTION_EVENT_CONTRACT,
} from './event_delegation_utils';
export {provideGlobalEventDelegation as ɵprovideGlobalEventDelegation} from './event_dispatch/event_delegation';
export {JSACTION_EVENT_CONTRACT as ɵJSACTION_EVENT_CONTRACT} from './event_delegation_utils';
export {IS_HYDRATION_DOM_REUSE_ENABLED as ɵIS_HYDRATION_DOM_REUSE_ENABLED} from './hydration/tokens';
export {
HydratedNode as ɵHydratedNode,

View file

@ -7,19 +7,10 @@
*/
// tslint:disable:no-duplicate-imports
import {
EventContract,
EventContractContainer,
EventDispatcher,
isEarlyEventType,
getActionCache,
registerDispatcher,
} from '@angular/core/primitives/event-dispatch';
import {EventContract} from '@angular/core/primitives/event-dispatch';
import {Attribute} from '@angular/core/primitives/event-dispatch';
import {Injectable, InjectionToken, Injector, inject} from './di';
import {InjectionToken} from './di';
import {RElement} from './render3/interfaces/renderer_dom';
import {EVENT_REPLAY_ENABLED_DEFAULT, IS_EVENT_REPLAY_ENABLED} from './hydration/tokens';
import {OnDestroy} from './interface/lifecycle_hooks';
declare global {
interface Element {
@ -71,60 +62,3 @@ export const JSACTION_EVENT_CONTRACT = new InjectionToken<EventContractDetails>(
factory: () => ({}),
},
);
export const GLOBAL_EVENT_DELEGATION = new InjectionToken<GlobalEventDelegation>(
ngDevMode ? 'GLOBAL_EVENT_DELEGATION' : '',
);
/**
* This class is the delegate for `EventDelegationPlugin`. It represents the
* noop version of this class, with the enabled version set when
* `provideGlobalEventDelegation` is called.
*/
@Injectable()
export class GlobalEventDelegation implements OnDestroy {
private eventContractDetails = inject(JSACTION_EVENT_CONTRACT);
ngOnDestroy() {
this.eventContractDetails.instance?.cleanUp();
}
supports(eventType: string): boolean {
return isEarlyEventType(eventType);
}
addEventListener(element: HTMLElement, eventType: string, handler: Function): Function {
// Note: contrary to the type, Window and Document can be passed in
// as well.
if (element.nodeType === Node.ELEMENT_NODE) {
this.eventContractDetails.instance!.addEvent(eventType);
getActionCache(element)[eventType] = '';
sharedStashFunction(element, eventType, handler);
} else {
element.addEventListener(eventType, handler as EventListener);
}
return () => this.removeEventListener(element, eventType, handler);
}
removeEventListener(element: HTMLElement, eventType: string, callback: Function): void {
if (element.nodeType === Node.ELEMENT_NODE) {
getActionCache(element)[eventType] = undefined;
} else {
element.removeEventListener(eventType, callback as EventListener);
}
}
}
export const initGlobalEventDelegation = (
eventContractDetails: EventContractDetails,
injector: Injector,
) => {
if (injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT)) {
return;
}
const eventContract = (eventContractDetails.instance = new EventContract(
new EventContractContainer(document.body),
));
const dispatcher = new EventDispatcher(invokeRegisteredListeners, /** clickModSupport */ false);
registerDispatcher(eventContract, dispatcher);
};

View file

@ -1,58 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {EventContract} from '@angular/core/primitives/event-dispatch';
import {ENVIRONMENT_INITIALIZER, Injector} from '../di';
import {inject} from '../di/injector_compatibility';
import {Provider} from '../di/interface/provider';
import {
GLOBAL_EVENT_DELEGATION,
GlobalEventDelegation,
JSACTION_EVENT_CONTRACT,
initGlobalEventDelegation,
} from '../event_delegation_utils';
import {IS_GLOBAL_EVENT_DELEGATION_ENABLED} from '../hydration/tokens';
declare global {
interface Window {
__jsaction_contract: EventContract | undefined;
}
}
/**
* Returns a set of providers required to setup support for event delegation.
* @param multiContract - Experimental support to provide one event contract
* when there are multiple binaries on the page.
*/
export function provideGlobalEventDelegation(multiContract = false): Provider[] {
return [
{
provide: IS_GLOBAL_EVENT_DELEGATION_ENABLED,
useValue: true,
},
{
provide: ENVIRONMENT_INITIALIZER,
useValue: () => {
const injector = inject(Injector);
const eventContractDetails = injector.get(JSACTION_EVENT_CONTRACT);
if (multiContract && window.__jsaction_contract) {
eventContractDetails.instance = window.__jsaction_contract;
return;
}
initGlobalEventDelegation(eventContractDetails, injector);
window.__jsaction_contract = eventContractDetails.instance;
},
multi: true,
},
{
provide: GLOBAL_EVENT_DELEGATION,
useClass: GlobalEventDelegation,
},
];
}

View file

@ -27,11 +27,7 @@ import {CLEANUP, LView, TView} from '../render3/interfaces/view';
import {isPlatformBrowser} from '../render3/util/misc_utils';
import {unwrapRNode} from '../render3/util/view_utils';
import {
EVENT_REPLAY_ENABLED_DEFAULT,
IS_EVENT_REPLAY_ENABLED,
IS_GLOBAL_EVENT_DELEGATION_ENABLED,
} from './tokens';
import {EVENT_REPLAY_ENABLED_DEFAULT, IS_EVENT_REPLAY_ENABLED} from './tokens';
import {
sharedStashFunction,
removeListeners,
@ -47,18 +43,11 @@ import {performanceMarkFeature} from '../util/performance';
*/
const jsactionSet = new Set<Element>();
function isGlobalEventDelegationEnabled(injector: Injector) {
return injector.get(IS_GLOBAL_EVENT_DELEGATION_ENABLED, false);
}
/**
* Determines whether Event Replay feature should be activated on the client.
*/
function shouldEnableEventReplay(injector: Injector) {
return (
injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT) &&
!isGlobalEventDelegationEnabled(injector)
);
return injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT);
}
/**

View file

@ -49,10 +49,3 @@ export const IS_EVENT_REPLAY_ENABLED = new InjectionToken<boolean>(
);
export const EVENT_REPLAY_ENABLED_DEFAULT = false;
/**
* Internal token that indicates whether global event delegation support is enabled.
*/
export const IS_GLOBAL_EVENT_DELEGATION_ENABLED = new InjectionToken<boolean>(
typeof ngDevMode === 'undefined' || !!ngDevMode ? 'IS_GLOBAL_EVENT_DELEGATION_ENABLED' : '',
);

View file

@ -179,9 +179,6 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
@ -236,9 +233,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -251,9 +245,6 @@
{
"name": "FALSE_BOOLEAN_VALUES"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -257,9 +257,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -272,9 +269,6 @@
{
"name": "FALSE_BOOLEAN_VALUES"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -164,9 +164,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -176,9 +173,6 @@
{
"name": "EventManagerPlugin"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -197,9 +197,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -209,9 +206,6 @@
{
"name": "EventManagerPlugin"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},
@ -1436,9 +1430,6 @@
{
"name": "init_event_contract_defines"
},
{
"name": "init_event_delegation"
},
{
"name": "init_event_delegation_utils"
},

View file

@ -224,9 +224,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -272,9 +269,6 @@
{
"name": "FormsExampleModule"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -227,9 +227,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -251,9 +248,6 @@
{
"name": "FormsModule"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -125,9 +125,6 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
@ -170,9 +167,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -182,9 +176,6 @@
{
"name": "EventManagerPlugin"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -188,9 +188,6 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
@ -236,9 +233,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -251,9 +245,6 @@
{
"name": "EventType2"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -107,9 +107,6 @@
{
"name": "DomAdapter"
},
{
"name": "DomEventsPlugin"
},
{
"name": "DomRendererFactory2"
},
@ -149,9 +146,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -161,9 +155,6 @@
{
"name": "EventManagerPlugin"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -167,9 +167,6 @@
{
"name": "ErrorHandler"
},
{
"name": "EventDelegationPlugin"
},
{
"name": "EventEmitter"
},
@ -179,9 +176,6 @@
{
"name": "EventManagerPlugin"
},
{
"name": "GLOBAL_EVENT_DELEGATION"
},
{
"name": "GenericBrowserDomAdapter"
},

View file

@ -1,20 +0,0 @@
load("//tools:defaults.bzl", "karma_web_test_suite", "ts_library")
ts_library(
name = "test_lib",
testonly = True,
srcs = [
"event_dispatch_spec.ts",
],
deps = [
"//packages/core",
"//packages/core/primitives/event-dispatch",
"//packages/core/testing",
"//packages/platform-browser",
],
)
karma_web_test_suite(
name = "test",
deps = [":test_lib"],
)

View file

@ -1,303 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {
Component,
HostListener,
Renderer2,
inject,
ɵprovideGlobalEventDelegation,
} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
function configureTestingModule(components: unknown[]) {
TestBed.configureTestingModule({
imports: components,
providers: [ɵprovideGlobalEventDelegation()],
});
}
describe('event dispatch', () => {
let fixture: ComponentFixture<unknown>;
it(`executes an onclick handler`, async () => {
const onClickSpy = jasmine.createSpy();
@Component({
selector: 'app',
standalone: true,
template: `
<button id="btn" (click)="onClick()"></button>
`,
})
class AppComponent {
onClick = onClickSpy;
}
configureTestingModule([AppComponent]);
const addEventListenerSpy = spyOn(
HTMLButtonElement.prototype,
'addEventListener',
).and.callThrough();
fixture = TestBed.createComponent(AppComponent);
const button = (fixture.debugElement.nativeElement as Element)
.firstElementChild as HTMLButtonElement;
button.click();
expect(onClickSpy).toHaveBeenCalledTimes(1);
expect(addEventListenerSpy).not.toHaveBeenCalled();
});
it('should work for elements with local refs', async () => {
const onClickSpy = jasmine.createSpy();
@Component({
selector: 'app',
standalone: true,
template: `
<button id="btn" (click)="onClick()" #localRef></button>
`,
})
class AppComponent {
onClick = onClickSpy;
}
configureTestingModule([AppComponent]);
fixture = TestBed.createComponent(AppComponent);
fixture.debugElement.nativeElement.querySelector('#btn').click();
expect(onClickSpy).toHaveBeenCalled();
});
it('should route to the appropriate component with content projection', async () => {
const outerOnClickSpy = jasmine.createSpy();
const innerOnClickSpy = jasmine.createSpy();
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<button id="inner-button" (click)="onClick()"></button>
<ng-content></ng-content>
</div>
`,
})
class CardComponent {
onClick = innerOnClickSpy;
}
@Component({
selector: 'app',
imports: [CardComponent],
standalone: true,
template: `
<app-card>
<h2>Card Title</h2>
<p>This is some card content.</p>
<button id="outer-button" (click)="onClick()">Click Me</button>
</app-card>
`,
})
class AppComponent {
onClick = outerOnClickSpy;
}
configureTestingModule([AppComponent]);
fixture = TestBed.createComponent(AppComponent);
const nativeElement = fixture.debugElement.nativeElement;
const outer = nativeElement.querySelector('#outer-button')!;
const inner = nativeElement.querySelector('#inner-button')!;
outer.click();
inner.click();
expect(outerOnClickSpy).toHaveBeenCalledBefore(innerOnClickSpy);
});
describe('bubbling behavior', () => {
it('should propagate events', async () => {
const onClickSpy = jasmine.createSpy();
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top" (click)="onClick()">
<div id="bottom" (click)="onClick()"></div>
</div>
`,
})
class SimpleComponent {
onClick = onClickSpy;
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(2);
});
it('should not propagate events if stopPropagation is called', async () => {
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top" (click)="onClick($event)">
<div id="bottom" (click)="onClick($event)"></div>
</div>
`,
})
class SimpleComponent {
onClick(e: Event) {
e.stopPropagation();
}
}
const onClickSpy = spyOn(SimpleComponent.prototype, 'onClick').and.callThrough();
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
it('should call the original stopPropagation method', async () => {
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top" (click)="onClick($event)">
<div id="bottom" (click)="onClick($event)"></div>
</div>
`,
})
class SimpleComponent {
onClick(e: Event) {
e.stopPropagation();
e.preventDefault();
}
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
const event = new MouseEvent('click', {bubbles: true});
spyOn(event, 'stopPropagation');
const stopPropagation = event.stopPropagation;
spyOn(event, 'preventDefault');
const preventDefault = event.preventDefault;
bottomEl.dispatchEvent(event);
expect(stopPropagation).toHaveBeenCalled();
expect(preventDefault).toHaveBeenCalled();
});
});
describe('manual listening', () => {
it('should trigger events when manually registered', async () => {
const onClickSpy = jasmine.createSpy();
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top">
<div id="bottom"></div>
</div>
`,
})
class SimpleComponent {
renderer = inject(Renderer2);
destroy!: Function;
listen(el: Element) {
this.destroy = this.renderer.listen(el, 'click', onClickSpy);
}
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
(fixture.componentInstance as SimpleComponent).listen(nativeElement);
const bottomEl = nativeElement.querySelector('#bottom')!;
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(1);
bottomEl.dispatchEvent(new MouseEvent('click', {bubbles: true, shiftKey: true}));
expect(onClickSpy).toHaveBeenCalledTimes(2);
(fixture.componentInstance as SimpleComponent).destroy();
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(2);
});
it('should allow host listening on the window', async () => {
const onClickSpy = jasmine.createSpy();
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top">
<div id="bottom"></div>
</div>
`,
})
class SimpleComponent {
renderer = inject(Renderer2);
destroy!: Function;
@HostListener('window:click', ['$event.target'])
listen(el: Element) {
onClickSpy();
}
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
it('should allow host listening on the window', async () => {
const onClickSpy = jasmine.createSpy();
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top">
<div id="bottom"></div>
</div>
`,
})
class SimpleComponent {
renderer = inject(Renderer2);
destroy!: Function;
@HostListener('window:click', ['$event.target'])
listen(el: Element) {
onClickSpy();
}
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
bottomEl.click();
expect(onClickSpy).toHaveBeenCalledTimes(1);
});
});
});
describe('capture behavior', () => {
let fixture: ComponentFixture<unknown>;
it('should not bubble', async () => {
const onFocusSpy = jasmine.createSpy();
@Component({
standalone: true,
selector: 'app',
template: `
<div id="top" (focus)="onFocus()">
<div id="bottom"></div>
</div>
`,
})
class SimpleComponent {
onFocus = onFocusSpy;
}
configureTestingModule([SimpleComponent]);
fixture = TestBed.createComponent(SimpleComponent);
const nativeElement = fixture.debugElement.nativeElement;
const bottomEl = nativeElement.querySelector('#bottom')!;
const topEl = nativeElement.querySelector('#top')!;
bottomEl.dispatchEvent(new FocusEvent('focus'));
expect(onFocusSpy).toHaveBeenCalledTimes(0);
topEl.dispatchEvent(new FocusEvent('focus'));
expect(onFocusSpy).toHaveBeenCalledTimes(1);
});
});

View file

@ -49,7 +49,6 @@ import {BrowserGetTestability} from './browser/testability';
import {BrowserXhr} from './browser/xhr';
import {DomRendererFactory2} from './dom/dom_renderer';
import {DomEventsPlugin} from './dom/events/dom_events';
import {EventDelegationPlugin} from './dom/events/event_delegation';
import {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
import {KeyEventsPlugin} from './dom/events/key_events';
import {SharedStylesHost} from './dom/shared_styles_host';
@ -241,11 +240,6 @@ const BROWSER_MODULE_PROVIDERS: Provider[] = [
deps: [DOCUMENT, NgZone, PLATFORM_ID],
},
{provide: EVENT_MANAGER_PLUGINS, useClass: KeyEventsPlugin, multi: true, deps: [DOCUMENT]},
{
provide: EVENT_MANAGER_PLUGINS,
useClass: EventDelegationPlugin,
multi: true,
},
DomRendererFactory2,
SharedStylesHost,
EventManager,

View file

@ -1,33 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {Inject, Injectable, inject, ɵGLOBAL_EVENT_DELEGATION} from '@angular/core';
import {EventManagerPlugin} from './event_manager';
import {DOCUMENT} from '@angular/common';
@Injectable()
export class EventDelegationPlugin extends EventManagerPlugin {
private delegate = inject(ɵGLOBAL_EVENT_DELEGATION, {optional: true});
constructor(@Inject(DOCUMENT) doc: any) {
super(doc);
}
override supports(eventName: string): boolean {
// If `GlobalDelegationEventPlugin` implementation is not provided,
// this plugin is kept disabled.
return this.delegate ? this.delegate.supports(eventName) : false;
}
override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
return this.delegate!.addEventListener(element, eventName, handler);
}
removeEventListener(element: HTMLElement, eventName: string, callback: Function): void {
return this.delegate!.removeEventListener(element, eventName, callback);
}
}