66 * found in the LICENSE file at https://angular.io/license
77 */
88
9- import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
9+ import { coerceBooleanProperty , coerceNumberProperty } from '@angular/cdk/coercion' ;
1010import {
1111 AfterContentInit ,
1212 Directive ,
@@ -16,12 +16,10 @@ import {
1616 Input ,
1717 NgModule ,
1818 NgZone ,
19- OnChanges ,
2019 OnDestroy ,
2120 Output ,
22- SimpleChanges ,
2321} from '@angular/core' ;
24- import { Subject } from 'rxjs' ;
22+ import { Observable , Subject , Subscription } from 'rxjs' ;
2523import { debounceTime } from 'rxjs/operators' ;
2624
2725/**
@@ -35,6 +33,88 @@ export class MutationObserverFactory {
3533 }
3634}
3735
36+
37+ /** An injectable service that allows watching elements for changes to their content. */
38+ @Injectable ( { providedIn : 'root' } )
39+ export class ContentObserver implements OnDestroy {
40+ /** Keeps track of the existing MutationObservers so they can be reused. */
41+ private _observedElements = new Map < Element , {
42+ observer : MutationObserver | null ,
43+ stream : Subject < MutationRecord [ ] > ,
44+ count : number
45+ } > ( ) ;
46+
47+ constructor ( private _mutationObserverFactory : MutationObserverFactory ) { }
48+
49+ ngOnDestroy ( ) {
50+ this . _observedElements . forEach ( ( _ , element ) => this . _cleanupObserver ( element ) ) ;
51+ }
52+
53+ /**
54+ * Observe content changes on an element.
55+ * @param element The element to observe for content changes.
56+ */
57+ observe ( element : Element ) : Observable < MutationRecord [ ] > {
58+ return Observable . create ( observer => {
59+ const stream = this . _observeElement ( element ) ;
60+ const subscription = stream . subscribe ( observer ) ;
61+
62+ return ( ) => {
63+ subscription . unsubscribe ( ) ;
64+ this . _unobserveElement ( element ) ;
65+ } ;
66+ } ) ;
67+ }
68+
69+ /**
70+ * Observes the given element by using the existing MutationObserver if available, or creating a
71+ * new one if not.
72+ */
73+ private _observeElement ( element : Element ) : Subject < MutationRecord [ ] > {
74+ if ( ! this . _observedElements . has ( element ) ) {
75+ const stream = new Subject < MutationRecord [ ] > ( ) ;
76+ const observer = this . _mutationObserverFactory . create ( mutations => stream . next ( mutations ) ) ;
77+ if ( observer ) {
78+ observer . observe ( element , {
79+ characterData : true ,
80+ childList : true ,
81+ subtree : true
82+ } ) ;
83+ }
84+ this . _observedElements . set ( element , { observer, stream, count : 1 } ) ;
85+ } else {
86+ this . _observedElements . get ( element ) ! . count ++ ;
87+ }
88+ return this . _observedElements . get ( element ) ! . stream ;
89+ }
90+
91+ /**
92+ * Un-observes the given element and cleans up the underlying MutationObserver if nobody else is
93+ * observing this element.
94+ */
95+ private _unobserveElement ( element : Element ) {
96+ if ( this . _observedElements . has ( element ) ) {
97+ this . _observedElements . get ( element ) ! . count -- ;
98+ if ( ! this . _observedElements . get ( element ) ! . count ) {
99+ this . _cleanupObserver ( element ) ;
100+ }
101+ }
102+ }
103+
104+ /** Clean up the underlying MutationObserver for the specified element. */
105+ private _cleanupObserver ( element : Element ) {
106+ if ( this . _observedElements . has ( element ) ) {
107+ const { observer, stream} = this . _observedElements . get ( element ) ! ;
108+ if ( observer ) {
109+ observer . disconnect ( ) ;
110+ }
111+ stream . complete ( ) ;
112+ this . _observedElements . delete ( element ) ;
113+ }
114+ }
115+ }
116+
117+
38118/**
39119 * Directive that triggers a callback whenever the content of
40120 * its associated element has changed.
@@ -43,10 +123,7 @@ export class MutationObserverFactory {
43123 selector : '[cdkObserveContent]' ,
44124 exportAs : 'cdkObserveContent' ,
45125} )
46- export class CdkObserveContent implements AfterContentInit , OnChanges , OnDestroy {
47- private _observer : MutationObserver | null ;
48- private _disabled = false ;
49-
126+ export class CdkObserveContent implements AfterContentInit , OnDestroy {
50127 /** Event emitted for each change in the element's content. */
51128 @Output ( 'cdkObserveContent' ) event = new EventEmitter < MutationRecord [ ] > ( ) ;
52129
@@ -58,64 +135,55 @@ export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy
58135 get disabled ( ) { return this . _disabled ; }
59136 set disabled ( value : any ) {
60137 this . _disabled = coerceBooleanProperty ( value ) ;
138+ if ( this . _disabled ) {
139+ this . _unsubscribe ( ) ;
140+ } else {
141+ this . _subscribe ( ) ;
142+ }
61143 }
62-
63- /** Used for debouncing the emitted values to the observeContent event. */
64- private _debouncer = new Subject < MutationRecord [ ] > ( ) ;
144+ private _disabled = false ;
65145
66146 /** Debounce interval for emitting the changes. */
67- @Input ( ) debounce : number ;
68-
69- constructor (
70- private _mutationObserverFactory : MutationObserverFactory ,
71- private _elementRef : ElementRef ,
72- private _ngZone : NgZone ) { }
147+ @Input ( )
148+ get debounce ( ) : number { return this . _debounce ; }
149+ set debounce ( value : number ) {
150+ this . _debounce = coerceNumberProperty ( value ) ;
151+ this . _subscribe ( ) ;
152+ }
153+ private _debounce : number ;
73154
74- ngAfterContentInit ( ) {
75- if ( this . debounce > 0 ) {
76- this . _ngZone . runOutsideAngular ( ( ) => {
77- this . _debouncer . pipe ( debounceTime ( this . debounce ) )
78- . subscribe ( ( mutations : MutationRecord [ ] ) => this . event . emit ( mutations ) ) ;
79- } ) ;
80- } else {
81- this . _debouncer . subscribe ( mutations => this . event . emit ( mutations ) ) ;
82- }
155+ private _currentSubscription : Subscription | null = null ;
83156
84- this . _observer = this . _ngZone . runOutsideAngular ( ( ) => {
85- return this . _mutationObserverFactory . create ( ( mutations : MutationRecord [ ] ) => {
86- this . _debouncer . next ( mutations ) ;
87- } ) ;
88- } ) ;
157+ constructor ( private _contentObserver : ContentObserver , private _elementRef : ElementRef ,
158+ private _ngZone : NgZone ) { }
89159
90- if ( ! this . disabled ) {
91- this . _enable ( ) ;
92- }
93- }
94-
95- ngOnChanges ( changes : SimpleChanges ) {
96- if ( changes [ 'disabled' ] ) {
97- changes [ 'disabled' ] . currentValue ? this . _disable ( ) : this . _enable ( ) ;
160+ ngAfterContentInit ( ) {
161+ if ( ! this . _currentSubscription && ! this . disabled ) {
162+ this . _subscribe ( ) ;
98163 }
99164 }
100165
101166 ngOnDestroy ( ) {
102- this . _disable ( ) ;
103- this . _debouncer . complete ( ) ;
167+ this . _unsubscribe ( ) ;
104168 }
105169
106- private _disable ( ) {
107- if ( this . _observer ) {
108- this . _observer . disconnect ( ) ;
109- }
170+ private _subscribe ( ) {
171+ this . _unsubscribe ( ) ;
172+ const stream = this . _contentObserver . observe ( this . _elementRef . nativeElement ) ;
173+
174+ // TODO(mmalerba): We shouldn't be emitting on this @Output() outside the zone.
175+ // Consider brining it back inside the zone next time we're making breaking changes.
176+ // Bringing it back inside can cause things like infinite change detection loops and changed
177+ // after checked errors if people's code isn't handling it properly.
178+ this . _ngZone . runOutsideAngular ( ( ) => {
179+ this . _currentSubscription =
180+ ( this . debounce ? stream . pipe ( debounceTime ( this . debounce ) ) : stream ) . subscribe ( this . event ) ;
181+ } ) ;
110182 }
111183
112- private _enable ( ) {
113- if ( this . _observer ) {
114- this . _observer . observe ( this . _elementRef . nativeElement , {
115- characterData : true ,
116- childList : true ,
117- subtree : true
118- } ) ;
184+ private _unsubscribe ( ) {
185+ if ( this . _currentSubscription ) {
186+ this . _currentSubscription . unsubscribe ( ) ;
119187 }
120188 }
121189}
0 commit comments