@@ -16,9 +16,12 @@ import {MatchMedia} from '../match-media/match-media';
1616import { MediaChange } from '../media-change' ;
1717
1818type Builder = Function ;
19+ type ClearCallback = ( ) => void ;
20+ type UpdateCallback = ( val : any ) => void ;
1921type ValueMap = Map < string , string > ;
2022type BreakpointMap = Map < string , ValueMap > ;
2123type ElementMap = Map < HTMLElement , BreakpointMap > ;
24+ type ElementKeyMap = WeakMap < HTMLElement , Set < string > > ;
2225type SubscriptionMap = Map < string , Subscription > ;
2326type WatcherMap = WeakMap < HTMLElement , SubscriptionMap > ;
2427type BuilderMap = WeakMap < HTMLElement , Map < string , Builder > > ;
@@ -37,8 +40,11 @@ export interface ElementMatcher {
3740export class MediaMarshaller {
3841 private activatedBreakpoints : BreakPoint [ ] = [ ] ;
3942 private elementMap : ElementMap = new Map ( ) ;
43+ private elementKeyMap : ElementKeyMap = new WeakMap ( ) ;
44+ // registry of special triggers to update elements
4045 private watcherMap : WatcherMap = new WeakMap ( ) ;
4146 private builderMap : BuilderMap = new WeakMap ( ) ;
47+ private clearBuilderMap : BuilderMap = new WeakMap ( ) ;
4248 private subject : Subject < ElementMatcher > = new Subject ( ) ;
4349
4450 get activatedBreakpoint ( ) : string {
@@ -47,7 +53,9 @@ export class MediaMarshaller {
4753
4854 constructor ( protected matchMedia : MatchMedia ,
4955 protected breakpoints : BreakPointRegistry ) {
50- this . matchMedia . observe ( ) . subscribe ( this . activate . bind ( this ) ) ;
56+ this . matchMedia
57+ . observe ( )
58+ . subscribe ( this . activate . bind ( this ) ) ;
5159 this . registerBreakpoints ( ) ;
5260 }
5361
@@ -71,36 +79,19 @@ export class MediaMarshaller {
7179 * initialize the marshaller with necessary elements for delegation on an element
7280 * @param element
7381 * @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)
7685 */
7786 init ( element : HTMLElement ,
7887 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 ) ;
10495 }
10596
10697 /**
@@ -157,6 +148,7 @@ export class MediaMarshaller {
157148 this . updateElement ( element , key , this . getValue ( element , key ) ) ;
158149 }
159150
151+ /** Track element value changes for a specific key */
160152 trackValue ( element : HTMLElement , key : string ) : Observable < ElementMatcher > {
161153 return this . subject . asObservable ( )
162154 . pipe ( filter ( v => v . element === element && v . key === key ) ) ;
@@ -166,12 +158,41 @@ export class MediaMarshaller {
166158 updateStyles ( ) : void {
167159 this . elementMap . forEach ( ( bpMap , el ) => {
168160 const valueMap = this . getFallback ( bpMap ) ;
161+ const keyMap = new Set ( this . elementKeyMap . get ( el ) ! ) ;
169162 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+ } ) ;
171167 }
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+ } ) ;
172177 } ) ;
173178 }
174179
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+
175196 /**
176197 * update a given element with the activated values for a given key
177198 * @param element
@@ -206,6 +227,42 @@ export class MediaMarshaller {
206227 }
207228 }
208229
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+
209266 /** Breakpoint locator by mediaQuery */
210267 private findByQuery ( query : string ) {
211268 return this . breakpoints . findByQuery ( query ) ;
@@ -234,3 +291,17 @@ export class MediaMarshaller {
234291 this . matchMedia . registerQuery ( queries ) ;
235292 }
236293}
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