Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Commit d322ea7

Browse files
authored
fix(core): clear recent styles after responsive deactivation (#927)
Fixes #697 Fixes #296
1 parent 146cb16 commit d322ea7

File tree

26 files changed

+250
-82
lines changed

26 files changed

+250
-82
lines changed

src/lib/core/base/base2.ts

+29-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {ElementRef, OnChanges, OnDestroy, SimpleChanges} from '@angular/core';
9-
import {Subject} from 'rxjs';
9+
import {Observable, Subject} from 'rxjs';
1010

1111
import {StyleDefinition, StyleUtils} from '../style-utils/style-utils';
1212
import {StyleBuilder} from '../style-builder/style-builder';
@@ -17,6 +17,8 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
1717

1818
protected DIRECTIVE_KEY = '';
1919
protected inputs: string[] = [];
20+
/** The most recently used styles for the builder */
21+
protected mru: StyleDefinition = {};
2022
protected destroySubject: Subject<void> = new Subject();
2123

2224
/** Access to host element's parent DOM node */
@@ -64,6 +66,17 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
6466
this.marshal.releaseElement(this.nativeElement);
6567
}
6668

69+
/** Register with central marshaller service */
70+
protected init(extraTriggers: Observable<any>[] = []): void {
71+
this.marshal.init(
72+
this.elementRef.nativeElement,
73+
this.DIRECTIVE_KEY,
74+
this.updateWithValue.bind(this),
75+
this.clearStyles.bind(this),
76+
extraTriggers
77+
);
78+
}
79+
6780
/** Add styles to the element using predefined style builder */
6881
protected addStyles(input: string, parent?: Object) {
6982
const builder = this.styleBuilder;
@@ -78,10 +91,21 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
7891
}
7992
}
8093

94+
this.mru = {...genStyles};
8195
this.applyStyleToElement(genStyles);
8296
builder.sideEffect(input, genStyles, parent);
8397
}
8498

99+
/** Remove generated styles from an element using predefined style builder */
100+
protected clearStyles() {
101+
Object.keys(this.mru).forEach(k => {
102+
this.mru[k] = '';
103+
});
104+
this.applyStyleToElement(this.mru);
105+
this.mru = {};
106+
}
107+
108+
/** Force trigger style updates on DOM element */
85109
protected triggerUpdate() {
86110
const val = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY);
87111
this.marshal.updateElement(this.nativeElement, this.DIRECTIVE_KEY, val);
@@ -119,4 +143,8 @@ export abstract class BaseDirective2 implements OnChanges, OnDestroy {
119143
protected setValue(val: any, bp: string): void {
120144
this.marshal.setValue(this.nativeElement, this.DIRECTIVE_KEY, val, bp);
121145
}
146+
147+
protected updateWithValue(input: string) {
148+
this.addStyles(input);
149+
}
122150
}

src/lib/core/media-marshaller/media-marshaller.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ describe('media-marshaller', () => {
6666
const builder = () => {
6767
triggered = true;
6868
};
69-
mediaMarshaller.init(fakeElement, fakeKey, builder, [obs]);
69+
mediaMarshaller.init(fakeElement, fakeKey, builder, () => {}, [obs]);
7070
subject.next();
7171
expect(triggered).toBeTruthy();
7272
});
@@ -119,7 +119,7 @@ describe('media-marshaller', () => {
119119
const builder = () => {
120120
triggered = true;
121121
};
122-
mediaMarshaller.init(fakeElement, fakeKey, builder, [obs]);
122+
mediaMarshaller.init(fakeElement, fakeKey, builder, () => {}, [obs]);
123123
mediaMarshaller.releaseElement(fakeElement);
124124
subject.next();
125125
expect(triggered).toBeFalsy();

src/lib/core/media-marshaller/media-marshaller.ts

+100-29
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ import {MatchMedia} from '../match-media/match-media';
1616
import {MediaChange} from '../media-change';
1717

1818
type Builder = Function;
19+
type ClearCallback = () => void;
20+
type UpdateCallback = (val: any) => void;
1921
type ValueMap = Map<string, string>;
2022
type BreakpointMap = Map<string, ValueMap>;
2123
type ElementMap = Map<HTMLElement, BreakpointMap>;
24+
type ElementKeyMap = WeakMap<HTMLElement, Set<string>>;
2225
type SubscriptionMap = Map<string, Subscription>;
2326
type WatcherMap = WeakMap<HTMLElement, SubscriptionMap>;
2427
type BuilderMap = WeakMap<HTMLElement, Map<string, Builder>>;
@@ -37,8 +40,11 @@ export interface ElementMatcher {
3740
export 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+
}

src/lib/extended/class/class.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class ClassDirective extends BaseDirective2 implements DoCheck {
4848
this.iterableDiffers, this.keyValueDiffers, this.elementRef, this.renderer
4949
);
5050
}
51-
this.marshal.init(this.nativeElement, this.DIRECTIVE_KEY, this.updateWithValue.bind(this));
51+
this.init();
5252
}
5353

5454
protected updateWithValue(value: any) {

src/lib/extended/img-src/img-src.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ export class ImgSrcDirective extends BaseDirective2 {
4040
@Inject(PLATFORM_ID) protected platformId: Object,
4141
@Inject(SERVER_TOKEN) protected serverModuleLoaded: boolean) {
4242
super(elementRef, styleBuilder, styler, marshal);
43-
this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY,
44-
this.updateSrcFor.bind(this));
43+
this.init();
4544
this.setValue('', this.nativeElement.getAttribute('src') || '');
4645
if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {
4746
this.nativeElement.setAttribute('src', '');
@@ -56,7 +55,7 @@ export class ImgSrcDirective extends BaseDirective2 {
5655
* Do nothing to standard `<img src="">` usages, only when responsive
5756
* keys are present do we actually call `setAttribute()`
5857
*/
59-
protected updateSrcFor() {
58+
protected updateWithValue() {
6059
let url = this.activatedValue || this.defaultSrc;
6160
if (isPlatformServer(this.platformId) && this.serverModuleLoaded) {
6261
this.addStyles(url);

src/lib/extended/show-hide/show-hide.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ export class ShowHideDirective extends BaseDirective2 implements AfterViewInit,
9797
DISPLAY_MAP.set(this.nativeElement, this.display);
9898
}
9999

100-
this.marshal.init(this.elementRef.nativeElement, this.DIRECTIVE_KEY,
101-
this.updateWithValue.bind(this));
100+
this.init();
102101
// set the default to show unless explicitly overridden
103102
const defaultValue = this.marshal.getValue(this.nativeElement, this.DIRECTIVE_KEY, '');
104103
if (defaultValue === undefined || defaultValue === '') {

src/lib/extended/style/style.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class StyleDirective extends BaseDirective2 implements DoCheck {
4949
// defined on the same host element; since the responsive variations may be defined...
5050
this.ngStyleInstance = new NgStyle(this.keyValueDiffers, this.elementRef, this.renderer);
5151
}
52-
this.marshal.init(this.nativeElement, this.DIRECTIVE_KEY, this.updateWithValue.bind(this));
52+
this.init();
5353
this.setValue(this.nativeElement.getAttribute('style') || '', '');
5454
}
5555

src/lib/flex/flex-align/flex-align.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ export class FlexAlignDirective extends BaseDirective2 {
6666
@Optional() protected styleBuilder: FlexAlignStyleBuilder,
6767
protected marshal: MediaMarshaller) {
6868
super(elRef, styleBuilder, styleUtils, marshal);
69-
this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY,
70-
this.addStyles.bind(this));
69+
this.init();
7170
}
7271

7372
protected styleCache = flexAlignCache;

src/lib/flex/flex-offset/flex-offset.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,11 @@ export class FlexOffsetDirective extends BaseDirective2 implements OnChanges {
7676
protected marshal: MediaMarshaller,
7777
protected styler: StyleUtils) {
7878
super(elRef, styleBuilder, styler, marshal);
79-
this.marshal.init(this.elRef.nativeElement, this.DIRECTIVE_KEY,
80-
this.updateWithValue.bind(this), [this.directionality.change]);
79+
this.init([this.directionality.change]);
80+
// Parent DOM `layout-gap` with affect the nested child with `flex-offset`
8181
if (this.parentElement) {
82-
this.marshal.trackValue(this.parentElement, 'layout-gap')
82+
this.marshal
83+
.trackValue(this.parentElement, 'layout-gap')
8384
.pipe(takeUntil(this.destroySubject))
8485
.subscribe(this.triggerUpdate.bind(this));
8586
}

0 commit comments

Comments
 (0)