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.