From 9b38c6a435f167e8c3b40331ff0d913d30b5a44d Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 19 Jan 2023 11:44:46 -0500 Subject: [PATCH] wip --- src/dev-app/performance/BUILD.bazel | 1 + src/dev-app/performance/performance-demo.html | 1022 ++++++++++++++++- src/dev-app/performance/performance-demo.ts | 8 +- src/material/core/option/index.ts | 6 +- src/material/core/option/option-parent.ts | 4 +- src/material/core/option/option.ts | 182 ++- 6 files changed, 1211 insertions(+), 12 deletions(-) diff --git a/src/dev-app/performance/BUILD.bazel b/src/dev-app/performance/BUILD.bazel index ffba0b8921aa..eececca8545f 100644 --- a/src/dev-app/performance/BUILD.bazel +++ b/src/dev-app/performance/BUILD.bazel @@ -12,6 +12,7 @@ ng_module( deps = [ "//src/material/button", "//src/material/divider", + "//src/material/expansion", "//src/material/form-field", "//src/material/icon", "//src/material/input", diff --git a/src/dev-app/performance/performance-demo.html b/src/dev-app/performance/performance-demo.html index 5674ab4b2232..235ebe90ed04 100644 --- a/src/dev-app/performance/performance-demo.html +++ b/src/dev-app/performance/performance-demo.html @@ -24,7 +24,7 @@ 1 10 100 - 1000 + 10000 @@ -82,9 +82,1023 @@ - - Input - + + Favorite food + + + {{option.text}} + + + + diff --git a/src/dev-app/performance/performance-demo.ts b/src/dev-app/performance/performance-demo.ts index 7e0b17bc4b68..cb6b84479b97 100644 --- a/src/dev-app/performance/performance-demo.ts +++ b/src/dev-app/performance/performance-demo.ts @@ -20,6 +20,7 @@ import {MatSelectModule} from '@angular/material/select'; import {MatTableDataSource, MatTableModule} from '@angular/material/table'; import {take} from 'rxjs/operators'; +import {MatExpansionModule} from '@angular/material/expansion'; @Component({ selector: 'performance-demo', @@ -31,6 +32,7 @@ import {take} from 'rxjs/operators'; FormsModule, MatButtonModule, MatDividerModule, + MatExpansionModule, MatFormFieldModule, MatIconModule, MatInputModule, @@ -44,10 +46,10 @@ export class PerformanceDemo implements AfterViewInit { show = false; /** The number of times metrics will be gathered. */ - sampleSize = 100; + sampleSize = 1; /** The number of components being rendered. */ - componentCount = 100; + componentCount = 1; /** A flat array of every sample recorded. */ allSamples: number[] = []; @@ -67,6 +69,8 @@ export class PerformanceDemo implements AfterViewInit { /** Used in an ngFor to render the desired number of comonents. */ componentArray = [].constructor(this.componentCount); + options = Array.from({length: 1000}).map((_, i) => ({text: `Option ${i}`, value: i})); + /** The standard deviation of the recorded samples. */ get stdev(): number | undefined { if (!this.allSamples.length) { diff --git a/src/material/core/option/index.ts b/src/material/core/option/index.ts index 96da5f1c97fc..8a0928485c8d 100644 --- a/src/material/core/option/index.ts +++ b/src/material/core/option/index.ts @@ -11,13 +11,13 @@ import {CommonModule} from '@angular/common'; import {MatRippleModule} from '../ripple/index'; import {MatPseudoCheckboxModule} from '../selection/index'; import {MatCommonModule} from '../common-behaviors/common-module'; -import {MatOption} from './option'; +import {MatLazyOption, MatOption} from './option'; import {MatOptgroup} from './optgroup'; @NgModule({ imports: [MatRippleModule, CommonModule, MatCommonModule, MatPseudoCheckboxModule], - exports: [MatOption, MatOptgroup], - declarations: [MatOption, MatOptgroup], + exports: [MatOption, MatLazyOption, MatOptgroup], + declarations: [MatOption, MatLazyOption, MatOptgroup], }) export class MatOptionModule {} diff --git a/src/material/core/option/option-parent.ts b/src/material/core/option/option-parent.ts index 3cb515aedd97..b47c4dc4a934 100644 --- a/src/material/core/option/option-parent.ts +++ b/src/material/core/option/option-parent.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {InjectionToken} from '@angular/core'; +import {EventEmitter, InjectionToken} from '@angular/core'; /** * Describes a parent component that manages a list of options. @@ -17,6 +17,8 @@ export interface MatOptionParentComponent { disableRipple?: boolean; multiple?: boolean; inertGroups?: boolean; + valueChange: EventEmitter; + openedChange: EventEmitter; } /** diff --git a/src/material/core/option/option.ts b/src/material/core/option/option.ts index 97e386d9599d..574957d40ad6 100644 --- a/src/material/core/option/option.ts +++ b/src/material/core/option/option.ts @@ -8,7 +8,7 @@ import {FocusableOption, FocusOrigin} from '@angular/cdk/a11y'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {ENTER, hasModifierKey, SPACE} from '@angular/cdk/keycodes'; +import {ENTER, hasModifierKey, P, SPACE} from '@angular/cdk/keycodes'; import { Component, ViewEncapsulation, @@ -25,10 +25,16 @@ import { EventEmitter, QueryList, ViewChild, + TemplateRef, + ViewContainerRef, + NgZone, + Injectable, } from '@angular/core'; -import {Subject} from 'rxjs'; +import {Observable, Subject, Subscription, fromEvent, observable} from 'rxjs'; import {MatOptgroup, MAT_OPTGROUP, _MatOptgroupBase} from './optgroup'; import {MatOptionParentComponent, MAT_OPTION_PARENT_COMPONENT} from './option-parent'; +import {finalize, share, takeUntil} from 'rxjs/operators'; +import {DOCUMENT} from '@angular/common'; /** * Option IDs need to be unique across components, so this counter exists outside of @@ -284,6 +290,178 @@ export class MatOption extends _MatOptionBase { } } +// @Directive({selector: '[lazy]'}) +// export class MatLazyOption { +// @Input() +// set lazy(value: boolean) { +// if (!value && !this._rendered.closed) { +// this._render(); +// } +// } + +// private readonly _rendered = new Subject(); + +// constructor( +// private _ngZone: NgZone, +// private templateRef: TemplateRef, +// private viewContainer: ViewContainerRef, +// @Optional() @Inject(MAT_OPTION_PARENT_COMPONENT) private parent: MatOptionParentComponent, +// ) { +// this.listen(); +// } + +// ngOnDestroy() { +// this._ngZone.runOutsideAngular(() => { +// this._rendered.next(); +// this._rendered.complete(); +// }); +// } + +// private listen(): void { +// this._ngZone.runOutsideAngular(() => { +// this.parent.openedChange.pipe(takeUntil(this._rendered)).subscribe(open => { +// if (open) { +// this._render(); +// } +// }); +// }); +// } + +// private _render(): void { +// if (!this._rendered.closed) { +// this.viewContainer.createEmbeddedView(this.templateRef); +// this._ngZone.runOutsideAngular(() => { +// this._rendered.next(); +// this._rendered.complete(); +// }); +// } +// } +// } + +@Directive({selector: '[lazy]'}) +export class MatLazyOption { + @Input('lazyTrigger') trigger: HTMLElement; + + @Input() + set lazy(isLazy: boolean) { + this._lazy = isLazy; + if (!isLazy) { + this._render(); + } + } + get lazy(): boolean { + return this._lazy; + } + private _lazy = true; + + private _focusSubscription?: Subscription; + private _mouseoverSubscription?: Subscription; + + constructor( + private _ngZone: NgZone, + private templateRef: TemplateRef, + private viewContainer: ViewContainerRef, + private listener: GlobalListener, + ) {} + + ngAfterViewInit() { + if (this.lazy) { + this.listen(); + } + } + + ngOnDestroy() { + this._ngZone.runOutsideAngular(() => { + this._focusSubscription?.unsubscribe(); + this._mouseoverSubscription?.unsubscribe(); + }); + } + + private listen(): void { + this._ngZone.runOutsideAngular(() => { + this._focusSubscription = this.listener.listen('focus', this.trigger, () => { + this._render(); + }); + this._mouseoverSubscription = this.listener.listen('mouseover', this.trigger, () => { + this._render(); + }); + }); + } + + private _render(): void { + if (!this._focusSubscription?.closed) { + console.log('render!'); + this.viewContainer.createEmbeddedView(this.templateRef); + this._ngZone.runOutsideAngular(() => { + this._focusSubscription?.unsubscribe(); + this._mouseoverSubscription?.unsubscribe(); + }); + } + } +} + +/** + * Provides a global listener for all events that occur on the document. + */ +@Injectable({providedIn: 'root'}) +export class GlobalListener implements OnDestroy { + private _elementToTypeToObservable = new Map< + HTMLElement | Document, + Map> + >(); + + /** The notifier that triggers the global event observables to stop emitting and complete. */ + private _destroyed = new Subject(); + + constructor(private _ngZone: NgZone) {} + + ngOnDestroy() { + this._destroyed.next(); + this._destroyed.complete(); + this._elementToTypeToObservable.clear(); + } + + /** + * Appends an event listener for events whose type attribute value is type. + * The callback argument sets the callback that will be invoked when the event is dispatched. + */ + listen( + type: keyof DocumentEventMap, + element: HTMLElement | Document, + listener: (ev: Event) => any, + ): Subscription { + // This is the first listener on this element. Instantiate the type-to-observable map. + if (!this._elementToTypeToObservable.has(element)) { + this._elementToTypeToObservable.set(element, new Map()); + } + + // This is the first listener of this type for this element. Instantiate the observable. + if (!this._elementToTypeToObservable.get(element)!.get(type)) { + const typeToObservable = this._elementToTypeToObservable.get(element)!; + typeToObservable.set(type, this._createGlobalEventObservable(element, type)); + } + + return this._ngZone.runOutsideAngular(() => + this._elementToTypeToObservable + .get(element)! + .get(type)! + .subscribe((event: Event) => listener(event)), + ); + } + + /** Creates an observable that emits all events of the given type. */ + private _createGlobalEventObservable( + element: HTMLElement | Document, + type: keyof DocumentEventMap, + ) { + return fromEvent(element, type, {passive: true, capture: true}).pipe( + takeUntil(this._destroyed), + finalize(() => this._elementToTypeToObservable.get(element)?.delete(type)), + share(), + ); + } +} + /** * Counts the amount of option group labels that precede the specified option. * @param optionIndex Index of the option at which to start counting.