6
6
* found in the LICENSE file at https://angular.io/license
7
7
*/
8
8
9
- import { coerceBooleanProperty } from '@angular/cdk/coercion' ;
9
+ import { coerceBooleanProperty , coerceNumberProperty } from '@angular/cdk/coercion' ;
10
10
import {
11
11
AfterContentInit ,
12
12
Directive ,
@@ -16,12 +16,10 @@ import {
16
16
Input ,
17
17
NgModule ,
18
18
NgZone ,
19
- OnChanges ,
20
19
OnDestroy ,
21
20
Output ,
22
- SimpleChanges ,
23
21
} from '@angular/core' ;
24
- import { Subject } from 'rxjs' ;
22
+ import { Observable , Subject , Subscription } from 'rxjs' ;
25
23
import { debounceTime } from 'rxjs/operators' ;
26
24
27
25
/**
@@ -35,6 +33,88 @@ export class MutationObserverFactory {
35
33
}
36
34
}
37
35
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
+
38
118
/**
39
119
* Directive that triggers a callback whenever the content of
40
120
* its associated element has changed.
@@ -43,10 +123,7 @@ export class MutationObserverFactory {
43
123
selector : '[cdkObserveContent]' ,
44
124
exportAs : 'cdkObserveContent' ,
45
125
} )
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 {
50
127
/** Event emitted for each change in the element's content. */
51
128
@Output ( 'cdkObserveContent' ) event = new EventEmitter < MutationRecord [ ] > ( ) ;
52
129
@@ -58,64 +135,55 @@ export class CdkObserveContent implements AfterContentInit, OnChanges, OnDestroy
58
135
get disabled ( ) { return this . _disabled ; }
59
136
set disabled ( value : any ) {
60
137
this . _disabled = coerceBooleanProperty ( value ) ;
138
+ if ( this . _disabled ) {
139
+ this . _unsubscribe ( ) ;
140
+ } else {
141
+ this . _subscribe ( ) ;
142
+ }
61
143
}
62
-
63
- /** Used for debouncing the emitted values to the observeContent event. */
64
- private _debouncer = new Subject < MutationRecord [ ] > ( ) ;
144
+ private _disabled = false ;
65
145
66
146
/** 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 ;
73
154
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 ;
83
156
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 ) { }
89
159
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 ( ) ;
98
163
}
99
164
}
100
165
101
166
ngOnDestroy ( ) {
102
- this . _disable ( ) ;
103
- this . _debouncer . complete ( ) ;
167
+ this . _unsubscribe ( ) ;
104
168
}
105
169
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
+ } ) ;
110
182
}
111
183
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 ( ) ;
119
187
}
120
188
}
121
189
}
0 commit comments