@@ -16,9 +16,12 @@ import {MatchMedia} from '../match-media/match-media';
16
16
import { MediaChange } from '../media-change' ;
17
17
18
18
type Builder = Function ;
19
+ type ClearCallback = ( ) => void ;
20
+ type UpdateCallback = ( val : any ) => void ;
19
21
type ValueMap = Map < string , string > ;
20
22
type BreakpointMap = Map < string , ValueMap > ;
21
23
type ElementMap = Map < HTMLElement , BreakpointMap > ;
24
+ type ElementKeyMap = WeakMap < HTMLElement , Set < string > > ;
22
25
type SubscriptionMap = Map < string , Subscription > ;
23
26
type WatcherMap = WeakMap < HTMLElement , SubscriptionMap > ;
24
27
type BuilderMap = WeakMap < HTMLElement , Map < string , Builder > > ;
@@ -37,8 +40,11 @@ export interface ElementMatcher {
37
40
export class MediaMarshaller {
38
41
private activatedBreakpoints : BreakPoint [ ] = [ ] ;
39
42
private elementMap : ElementMap = new Map ( ) ;
43
+ private elementKeyMap : ElementKeyMap = new WeakMap ( ) ;
44
+ // registry of special triggers to update elements
40
45
private watcherMap : WatcherMap = new WeakMap ( ) ;
41
46
private builderMap : BuilderMap = new WeakMap ( ) ;
47
+ private clearBuilderMap : BuilderMap = new WeakMap ( ) ;
42
48
private subject : Subject < ElementMatcher > = new Subject ( ) ;
43
49
44
50
get activatedBreakpoint ( ) : string {
@@ -47,7 +53,9 @@ export class MediaMarshaller {
47
53
48
54
constructor ( protected matchMedia : MatchMedia ,
49
55
protected breakpoints : BreakPointRegistry ) {
50
- this . matchMedia . observe ( ) . subscribe ( this . activate . bind ( this ) ) ;
56
+ this . matchMedia
57
+ . observe ( )
58
+ . subscribe ( this . activate . bind ( this ) ) ;
51
59
this . registerBreakpoints ( ) ;
52
60
}
53
61
@@ -71,36 +79,19 @@ export class MediaMarshaller {
71
79
* initialize the marshaller with necessary elements for delegation on an element
72
80
* @param element
73
81
* @param key
74
- * @param builder optional so that custom bp directives don't have to re-provide this
75
- * @param observables
82
+ * @param updateFn optional callback so that custom bp directives don't have to re-provide this
83
+ * @param clearFn optional callback so that custom bp directives don't have to re-provide this
84
+ * @param extraTriggers other triggers to force style updates (e.g. layout, directionality, etc)
76
85
*/
77
86
init ( element : HTMLElement ,
78
87
key : string ,
79
- builder ?: Builder ,
80
- observables : Observable < any > [ ] = [ ] ) : void {
81
- if ( builder ) {
82
- let builders = this . builderMap . get ( element ) ;
83
- if ( ! builders ) {
84
- builders = new Map ( ) ;
85
- this . builderMap . set ( element , builders ) ;
86
- }
87
- builders . set ( key , builder ) ;
88
- }
89
- if ( observables ) {
90
- let watchers = this . watcherMap . get ( element ) ;
91
- if ( ! watchers ) {
92
- watchers = new Map ( ) ;
93
- this . watcherMap . set ( element , watchers ) ;
94
- }
95
- const subscription = watchers . get ( key ) ;
96
- if ( ! subscription ) {
97
- const newSubscription = merge ( ...observables ) . subscribe ( ( ) => {
98
- const currentValue = this . getValue ( element , key ) ;
99
- this . updateElement ( element , key , currentValue ) ;
100
- } ) ;
101
- watchers . set ( key , newSubscription ) ;
102
- }
103
- }
88
+ updateFn ?: UpdateCallback ,
89
+ clearFn ?: ClearCallback ,
90
+ extraTriggers : Observable < any > [ ] = [ ] ) : void {
91
+ this . buildElementKeyMap ( element , key ) ;
92
+ initBuilderMap ( this . builderMap , element , key , updateFn ) ;
93
+ initBuilderMap ( this . clearBuilderMap , element , key , clearFn ) ;
94
+ this . watchExtraTriggers ( element , key , extraTriggers ) ;
104
95
}
105
96
106
97
/**
@@ -157,6 +148,7 @@ export class MediaMarshaller {
157
148
this . updateElement ( element , key , this . getValue ( element , key ) ) ;
158
149
}
159
150
151
+ /** Track element value changes for a specific key */
160
152
trackValue ( element : HTMLElement , key : string ) : Observable < ElementMatcher > {
161
153
return this . subject . asObservable ( )
162
154
. pipe ( filter ( v => v . element === element && v . key === key ) ) ;
@@ -166,12 +158,41 @@ export class MediaMarshaller {
166
158
updateStyles ( ) : void {
167
159
this . elementMap . forEach ( ( bpMap , el ) => {
168
160
const valueMap = this . getFallback ( bpMap ) ;
161
+ const keyMap = new Set ( this . elementKeyMap . get ( el ) ! ) ;
169
162
if ( valueMap ) {
170
- valueMap . forEach ( ( v , k ) => this . updateElement ( el , k , v ) ) ;
163
+ valueMap . forEach ( ( v , k ) => {
164
+ this . updateElement ( el , k , v ) ;
165
+ keyMap . delete ( k ) ;
166
+ } ) ;
171
167
}
168
+ keyMap . forEach ( k => {
169
+ const fallbackMap = this . getFallback ( bpMap , k ) ;
170
+ if ( fallbackMap ) {
171
+ const value = fallbackMap . get ( k ) ;
172
+ this . updateElement ( el , k , value ) ;
173
+ } else {
174
+ this . clearElement ( el , k ) ;
175
+ }
176
+ } ) ;
172
177
} ) ;
173
178
}
174
179
180
+ /**
181
+ * clear the styles for a given element
182
+ * @param element
183
+ * @param key
184
+ */
185
+ clearElement ( element : HTMLElement , key : string ) : void {
186
+ const builders = this . clearBuilderMap . get ( element ) ;
187
+ if ( builders ) {
188
+ const builder : Builder | undefined = builders . get ( key ) ;
189
+ if ( builder ) {
190
+ builder ( ) ;
191
+ this . subject . next ( { element, key, value : '' } ) ;
192
+ }
193
+ }
194
+ }
195
+
175
196
/**
176
197
* update a given element with the activated values for a given key
177
198
* @param element
@@ -206,6 +227,42 @@ export class MediaMarshaller {
206
227
}
207
228
}
208
229
230
+ /** Cross-reference for HTMLElement with directive key */
231
+ private buildElementKeyMap ( element : HTMLElement , key : string ) {
232
+ let keyMap = this . elementKeyMap . get ( element ) ;
233
+ if ( ! keyMap ) {
234
+ keyMap = new Set ( ) ;
235
+ this . elementKeyMap . set ( element , keyMap ) ;
236
+ }
237
+ keyMap . add ( key ) ;
238
+ }
239
+
240
+ /**
241
+ * Other triggers that should force style updates:
242
+ * - directionality
243
+ * - layout changes
244
+ * - mutationobserver updates
245
+ */
246
+ private watchExtraTriggers ( element : HTMLElement ,
247
+ key : string ,
248
+ triggers : Observable < any > [ ] ) {
249
+ if ( triggers && triggers . length ) {
250
+ let watchers = this . watcherMap . get ( element ) ;
251
+ if ( ! watchers ) {
252
+ watchers = new Map ( ) ;
253
+ this . watcherMap . set ( element , watchers ) ;
254
+ }
255
+ const subscription = watchers . get ( key ) ;
256
+ if ( ! subscription ) {
257
+ const newSubscription = merge ( ...triggers ) . subscribe ( ( ) => {
258
+ const currentValue = this . getValue ( element , key ) ;
259
+ this . updateElement ( element , key , currentValue ) ;
260
+ } ) ;
261
+ watchers . set ( key , newSubscription ) ;
262
+ }
263
+ }
264
+ }
265
+
209
266
/** Breakpoint locator by mediaQuery */
210
267
private findByQuery ( query : string ) {
211
268
return this . breakpoints . findByQuery ( query ) ;
@@ -234,3 +291,17 @@ export class MediaMarshaller {
234
291
this . matchMedia . registerQuery ( queries ) ;
235
292
}
236
293
}
294
+
295
+ function initBuilderMap ( map : BuilderMap ,
296
+ element : HTMLElement ,
297
+ key : string ,
298
+ input ?: UpdateCallback | ClearCallback ) : void {
299
+ if ( input !== undefined ) {
300
+ let oldMap = map . get ( element ) ;
301
+ if ( ! oldMap ) {
302
+ oldMap = new Map ( ) ;
303
+ map . set ( element , oldMap ) ;
304
+ }
305
+ oldMap . set ( key , input ) ;
306
+ }
307
+ }
0 commit comments