From 2d17a4854daaa7b9e0b59aa690826a0bdb8ce9c7 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Sat, 6 Jan 2018 20:58:45 -0500 Subject: [PATCH 1/2] refactor: remove private Angular APIs * Remove private API references to getDom() * Add PLATFORM_ID to all components --- src/lib/api/core/base-adapter.spec.ts | 2 +- src/lib/api/core/base-adapter.ts | 7 +- src/lib/api/core/base.ts | 20 ++-- src/lib/api/ext/class.ts | 12 ++- src/lib/api/ext/img-src.ts | 11 ++- src/lib/api/ext/show-hide.ts | 9 +- src/lib/api/ext/style.ts | 12 ++- src/lib/api/flexbox/flex-align.ts | 9 +- src/lib/api/flexbox/flex-fill.ts | 9 +- src/lib/api/flexbox/flex-offset.ts | 9 +- src/lib/api/flexbox/flex-order.ts | 9 +- src/lib/api/flexbox/flex.ts | 9 +- src/lib/api/flexbox/layout-align.ts | 10 +- src/lib/api/flexbox/layout-gap.ts | 7 +- src/lib/api/flexbox/layout-wrap.ts | 11 ++- src/lib/api/flexbox/layout.ts | 9 +- src/lib/media-query/match-media.ts | 99 ++++++++++--------- src/lib/media-query/mock/mock-match-media.ts | 10 +- src/lib/utils/style-utils.ts | 23 ++--- .../app/splitter/split.directive.ts | 18 +++- src/universal-app/app/util/helper.ts | 5 - 21 files changed, 194 insertions(+), 116 deletions(-) delete mode 100644 src/universal-app/app/util/helper.ts diff --git a/src/lib/api/core/base-adapter.spec.ts b/src/lib/api/core/base-adapter.spec.ts index e8c11d490..c7fcdd425 100644 --- a/src/lib/api/core/base-adapter.spec.ts +++ b/src/lib/api/core/base-adapter.spec.ts @@ -21,7 +21,7 @@ export class MockElementRef extends ElementRef { describe('BaseFxDirectiveAdapter class', () => { let component; beforeEach(() => { - component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as Renderer2); // tslint:disable-line:max-line-length + component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as Renderer2, {}); // tslint:disable-line:max-line-length }); describe('cacheInput', () => { it('should call _cacheInputArray when source is an array', () => { diff --git a/src/lib/api/core/base-adapter.ts b/src/lib/api/core/base-adapter.ts index 38f498d7e..82ca37698 100644 --- a/src/lib/api/core/base-adapter.ts +++ b/src/lib/api/core/base-adapter.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {ElementRef, Renderer2} from '@angular/core'; +import {ElementRef, Inject, PLATFORM_ID, Renderer2} from '@angular/core'; import {BaseFxDirective} from './base'; import {ResponsiveActivation} from './responsive-activation'; @@ -48,8 +48,9 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { constructor(protected _baseKey: string, // non-responsive @Input property name protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, - protected _renderer: Renderer2) { - super(_mediaMonitor, _elementRef, _renderer); + protected _renderer: Renderer2, + @Inject(PLATFORM_ID) protected _platformId: Object) { + super(_mediaMonitor, _elementRef, _renderer, _platformId); } /** diff --git a/src/lib/api/core/base.ts b/src/lib/api/core/base.ts index ef5e899ae..396403067 100644 --- a/src/lib/api/core/base.ts +++ b/src/lib/api/core/base.ts @@ -6,8 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ import { - ElementRef, OnDestroy, SimpleChanges, OnChanges, - SimpleChange, Renderer2 + ElementRef, + OnDestroy, + SimpleChanges, + OnChanges, + SimpleChange, + Renderer2, + Inject, + PLATFORM_ID, } from '@angular/core'; import {buildLayoutCSS} from '../../utils/layout-validator'; @@ -16,7 +22,8 @@ import { lookupStyle, lookupInlineStyle, applyStyleToElement, - applyStyleToElements, lookupAttributeValue + applyStyleToElements, + lookupAttributeValue, } from '../../utils/style-utils'; import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation'; @@ -63,7 +70,8 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ constructor(protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, - protected _renderer: Renderer2) { + protected _renderer: Renderer2, + @Inject(PLATFORM_ID) protected _platformId: Object) { } // ********************************************* @@ -133,7 +141,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * and optional restore it when the mediaQueries deactivate */ protected _getDisplayStyle(source: HTMLElement = this.nativeElement): string { - return lookupStyle(source || this.nativeElement, 'display'); + return lookupStyle(this._platformId, source || this.nativeElement, 'display'); } /** @@ -154,7 +162,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { let value = 'row'; if (target) { - value = lookupStyle(target, 'flex-direction') || 'row'; + value = lookupStyle(this._platformId, target, 'flex-direction') || 'row'; let hasInlineValue = lookupInlineStyle(target, 'flex-direction'); if (!hasInlineValue && addIfMissing) { diff --git a/src/lib/api/ext/class.ts b/src/lib/api/ext/class.ts index ae1cac2ca..c07636a14 100644 --- a/src/lib/api/ext/class.ts +++ b/src/lib/api/ext/class.ts @@ -17,7 +17,10 @@ import { Optional, Renderer2, SimpleChanges, - Self, OnInit + Self, + OnInit, + Inject, + PLATFORM_ID, } from '@angular/core'; import {NgClass} from '@angular/common'; @@ -91,8 +94,9 @@ export class ClassDirective extends BaseFxDirective protected _keyValueDiffers: KeyValueDiffers, protected _ngEl: ElementRef, protected _renderer: Renderer2, - @Optional() @Self() private _ngClassInstance: NgClass ) { - super(monitor, _ngEl, _renderer); + @Optional() @Self() private _ngClassInstance: NgClass, + @Inject(PLATFORM_ID) protected _platformId: Object) { + super(monitor, _ngEl, _renderer, _platformId); this._configureAdapters(); } @@ -135,7 +139,7 @@ export class ClassDirective extends BaseFxDirective */ protected _configureAdapters() { this._base = new BaseFxDirectiveAdapter( - 'ngClass', this.monitor, this._ngEl, this._renderer + 'ngClass', this.monitor, this._ngEl, this._renderer, this._platformId ); if (!this._ngClassInstance) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been defined on diff --git a/src/lib/api/ext/img-src.ts b/src/lib/api/ext/img-src.ts index c9cfb8f3c..b821bb3b4 100644 --- a/src/lib/api/ext/img-src.ts +++ b/src/lib/api/ext/img-src.ts @@ -11,7 +11,9 @@ import { Input, OnInit, OnChanges, - Renderer2 + Renderer2, + Inject, + PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; @@ -55,8 +57,11 @@ export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChange @Input('src.gt-lg') set srcGtLg(val) { this._cacheInput('srcGtLg', val); } /* tslint:enable */ - constructor(elRef: ElementRef, renderer: Renderer2, monitor: MediaMonitor) { - super(monitor, elRef, renderer); + constructor(elRef: ElementRef, + renderer: Renderer2, + monitor: MediaMonitor, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); this._cacheInput('src', elRef.nativeElement.getAttribute('src') || ''); } diff --git a/src/lib/api/ext/show-hide.ts b/src/lib/api/ext/show-hide.ts index 1ed4cfb8e..ca9ea65f9 100644 --- a/src/lib/api/ext/show-hide.ts +++ b/src/lib/api/ext/show-hide.ts @@ -15,7 +15,9 @@ import { Renderer2, SimpleChanges, Self, - Optional + Optional, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -104,9 +106,10 @@ export class ShowHideDirective extends BaseFxDirective implements OnInit, OnChan constructor(monitor: MediaMonitor, @Optional() @Self() protected _layout: LayoutDirective, protected elRef: ElementRef, - protected renderer: Renderer2) { + protected renderer: Renderer2, + @Inject(PLATFORM_ID) protected platformId: Object) { - super(monitor, elRef, renderer); + super(monitor, elRef, renderer, platformId); if (_layout) { /** diff --git a/src/lib/api/ext/style.ts b/src/lib/api/ext/style.ts index ee48a5ec0..e9c36dd23 100644 --- a/src/lib/api/ext/style.ts +++ b/src/lib/api/ext/style.ts @@ -17,7 +17,10 @@ import { Renderer2, SecurityContext, Self, - SimpleChanges, OnInit, + SimpleChanges, + OnInit, + Inject, + PLATFORM_ID, } from '@angular/core'; import {NgStyle} from '@angular/common'; @@ -89,9 +92,10 @@ export class StyleDirective extends BaseFxDirective protected _ngEl: ElementRef, protected _renderer: Renderer2, protected _differs: KeyValueDiffers, - @Optional() @Self() private _ngStyleInstance: NgStyle) { + @Optional() @Self() private _ngStyleInstance: NgStyle, + @Inject(PLATFORM_ID) protected _platformId: Object) { - super(monitor, _ngEl, _renderer); + super(monitor, _ngEl, _renderer, _platformId); this._configureAdapters(); } @@ -134,7 +138,7 @@ export class StyleDirective extends BaseFxDirective */ protected _configureAdapters() { this._base = new BaseFxDirectiveAdapter( - 'ngStyle', this.monitor, this._ngEl, this._renderer + 'ngStyle', this.monitor, this._ngEl, this._renderer, this._platformId ); if ( !this._ngStyleInstance ) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been diff --git a/src/lib/api/flexbox/flex-align.ts b/src/lib/api/flexbox/flex-align.ts index 701256ba9..ff4f30e69 100644 --- a/src/lib/api/flexbox/flex-align.ts +++ b/src/lib/api/flexbox/flex-align.ts @@ -14,6 +14,8 @@ import { OnDestroy, Renderer2, SimpleChanges, + Inject, + PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; @@ -54,8 +56,11 @@ export class FlexAlignDirective extends BaseFxDirective implements OnInit, OnCha @Input('fxFlexAlign.gt-lg') set alignGtLg(val) { this._cacheInput('alignGtLg', val); }; /* tslint:enable */ - constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2) { - super(monitor, elRef, renderer); + constructor(monitor: MediaMonitor, + elRef: ElementRef, + renderer: Renderer2, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); } diff --git a/src/lib/api/flexbox/flex-fill.ts b/src/lib/api/flexbox/flex-fill.ts index 5d1f955e8..8e73334df 100644 --- a/src/lib/api/flexbox/flex-fill.ts +++ b/src/lib/api/flexbox/flex-fill.ts @@ -5,7 +5,7 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Renderer2} from '@angular/core'; +import {Directive, ElementRef, Inject, PLATFORM_ID, Renderer2} from '@angular/core'; import {MediaMonitor} from '../../media-query/media-monitor'; import {BaseFxDirective} from '../core/base'; @@ -29,8 +29,11 @@ const FLEX_FILL_CSS = { [fxFlexFill] `}) export class FlexFillDirective extends BaseFxDirective { - constructor(monitor: MediaMonitor, public elRef: ElementRef, public renderer: Renderer2) { - super(monitor, elRef, renderer); + constructor(monitor: MediaMonitor, + public elRef: ElementRef, + public renderer: Renderer2, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); this._applyStyleToElement(FLEX_FILL_CSS); } } diff --git a/src/lib/api/flexbox/flex-offset.ts b/src/lib/api/flexbox/flex-offset.ts index b90917aa6..a97f8992e 100644 --- a/src/lib/api/flexbox/flex-offset.ts +++ b/src/lib/api/flexbox/flex-offset.ts @@ -15,7 +15,9 @@ import { Optional, Renderer2, SimpleChanges, - SkipSelf + SkipSelf, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -60,8 +62,9 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Optional() @SkipSelf() protected _container: LayoutDirective ) { - super(monitor, elRef, renderer); + @Optional() @SkipSelf() protected _container: LayoutDirective, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); this.watchParentFlow(); diff --git a/src/lib/api/flexbox/flex-order.ts b/src/lib/api/flexbox/flex-order.ts index e3e49654a..8c3953554 100644 --- a/src/lib/api/flexbox/flex-order.ts +++ b/src/lib/api/flexbox/flex-order.ts @@ -14,6 +14,8 @@ import { OnDestroy, Renderer2, SimpleChanges, + Inject, + PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; @@ -52,8 +54,11 @@ export class FlexOrderDirective extends BaseFxDirective implements OnInit, OnCha @Input('fxFlexOrder.lt-xl') set orderLtXl(val) { this._cacheInput('orderLtXl', val); }; /* tslint:enable */ - constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2) { - super(monitor, elRef, renderer); + constructor(monitor: MediaMonitor, + elRef: ElementRef, + renderer: Renderer2, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); } // ********************************************* diff --git a/src/lib/api/flexbox/flex.ts b/src/lib/api/flexbox/flex.ts index 410df3844..1ad8cf475 100644 --- a/src/lib/api/flexbox/flex.ts +++ b/src/lib/api/flexbox/flex.ts @@ -5,14 +5,16 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ - import { +import { Directive, ElementRef, + Inject, Input, OnChanges, OnDestroy, OnInit, Optional, + PLATFORM_ID, Renderer2, SimpleChanges, SkipSelf, @@ -86,9 +88,10 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, elRef: ElementRef, renderer: Renderer2, @Optional() @SkipSelf() protected _container: LayoutDirective, - @Optional() @SkipSelf() protected _wrap: LayoutWrapDirective) { + @Optional() @SkipSelf() protected _wrap: LayoutWrapDirective, + @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer); + super(monitor, elRef, renderer, platformId); this._cacheInput('flex', ''); this._cacheInput('shrink', 1); diff --git a/src/lib/api/flexbox/layout-align.ts b/src/lib/api/flexbox/layout-align.ts index c46e8fb15..cd18aacc0 100644 --- a/src/lib/api/flexbox/layout-align.ts +++ b/src/lib/api/flexbox/layout-align.ts @@ -14,7 +14,10 @@ import { OnInit, Optional, Renderer2, - SimpleChanges, Self, + SimpleChanges, + Self, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; import {extendObject} from '../../utils/object-extend'; @@ -68,8 +71,9 @@ export class LayoutAlignDirective extends BaseFxDirective implements OnInit, OnC constructor( monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Optional() @Self() container: LayoutDirective) { - super(monitor, elRef, renderer); + @Optional() @Self() container: LayoutDirective, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout-gap.ts b/src/lib/api/flexbox/layout-gap.ts index 19a5b04b7..7626ebe58 100644 --- a/src/lib/api/flexbox/layout-gap.ts +++ b/src/lib/api/flexbox/layout-gap.ts @@ -17,6 +17,8 @@ import { Optional, OnDestroy, NgZone, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -67,8 +69,9 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI elRef: ElementRef, renderer: Renderer2, @Optional() @Self() container: LayoutDirective, - private _zone: NgZone) { - super(monitor, elRef, renderer); + private _zone: NgZone, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout-wrap.ts b/src/lib/api/flexbox/layout-wrap.ts index ed4cfa489..991021a12 100644 --- a/src/lib/api/flexbox/layout-wrap.ts +++ b/src/lib/api/flexbox/layout-wrap.ts @@ -13,7 +13,11 @@ import { OnDestroy, OnInit, Renderer2, - SimpleChanges, Self, Optional, + SimpleChanges, + Self, + Optional, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -65,9 +69,10 @@ export class LayoutWrapDirective extends BaseFxDirective implements OnInit, OnCh monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Optional() @Self() container: LayoutDirective) { + @Optional() @Self() container: LayoutDirective, + @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer); + super(monitor, elRef, renderer, platformId); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout.ts b/src/lib/api/flexbox/layout.ts index c3aa688b2..62d6d5ae5 100644 --- a/src/lib/api/flexbox/layout.ts +++ b/src/lib/api/flexbox/layout.ts @@ -14,6 +14,8 @@ import { OnDestroy, Renderer2, SimpleChanges, + Inject, + PLATFORM_ID, } from '@angular/core'; import {Observable} from 'rxjs/Observable'; @@ -71,8 +73,11 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange /** * */ - constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2) { - super(monitor, elRef, renderer); + constructor(monitor: MediaMonitor, + elRef: ElementRef, + renderer: Renderer2, + @Inject(PLATFORM_ID) platformId: Object) { + super(monitor, elRef, renderer, platformId); this._announcer = new ReplaySubject(1); this.layout$ = this._announcer.asObservable(); } diff --git a/src/lib/media-query/match-media.ts b/src/lib/media-query/match-media.ts index d9dbf4d40..248b652dd 100644 --- a/src/lib/media-query/match-media.ts +++ b/src/lib/media-query/match-media.ts @@ -5,9 +5,16 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, NgZone} from '@angular/core'; -import {ɵgetDOM as getDom} from '@angular/platform-browser'; -import {DOCUMENT} from '@angular/common'; +import { + Inject, + Injectable, + NgZone, + PLATFORM_ID, + RendererFactory2, + RendererType2, + ViewEncapsulation, +} from '@angular/core'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; import {filter} from 'rxjs/operators/filter'; @@ -48,7 +55,10 @@ export class MatchMedia { protected _source: BehaviorSubject; protected _observable$: Observable; - constructor(protected _zone: NgZone, @Inject(DOCUMENT) protected _document: any) { + constructor(protected _zone: NgZone, + protected _rendererFactory: RendererFactory2, + @Inject(DOCUMENT) protected _document: any, + @Inject(PLATFORM_ID) protected _platformId: Object) { this._registry = new Map(); this._source = new BehaviorSubject(new MediaChange(true)); this._observable$ = this._source.asObservable(); @@ -90,7 +100,7 @@ export class MatchMedia { let list = normalizeQuery(mediaQuery); if (list.length > 0) { - prepareQueryCSS(list, this._document); + this._prepareQueryCSS(list, this._document); list.forEach(query => { let mql = this._registry.get(query); @@ -119,7 +129,8 @@ export class MatchMedia { * supports 0..n listeners for activation/deactivation */ protected _buildMQL(query: string): MediaQueryList { - let canListen = isBrowser() && !!(window).matchMedia('all').addListener; + let canListen = isPlatformBrowser(this._platformId) && + !!(window).matchMedia('all').addListener; return canListen ? (window).matchMedia(query) : { matches: query === 'all' || query === '', @@ -130,57 +141,57 @@ export class MatchMedia { } }; } -} - -/** - * Determine if SSR or Browser rendering. - */ -export function isBrowser() { - return getDom().supportsDOMEvents(); -} - -/** - * Private global registry for all dynamically-created, injected style tags - * @see prepare(query) - */ -const ALL_STYLES = {}; - -/** - * For Webkit engines that only trigger the MediaQueryList Listener - * when there is at least one CSS selector for the respective media query. - * - * @param query string The mediaQuery used to create a faux CSS selector - * - */ -function prepareQueryCSS(mediaQueries: string[], _document: any) { - let list = mediaQueries.filter(it => !ALL_STYLES[it]); - if (list.length > 0) { - let query = list.join(', '); - try { - let styleEl = getDom().createElement('style'); + /** + * For Webkit engines that only trigger the MediaQueryList Listener + * when there is at least one CSS selector for the respective media query. + * + * @param query string The mediaQuery used to create a faux CSS selector + * + */ + protected _prepareQueryCSS(mediaQueries: string[], _document: any) { + let list = mediaQueries.filter(it => !ALL_STYLES[it]); + if (list.length > 0) { + let query = list.join(', '); + + try { + const renderType: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} + }; + const renderer = this._rendererFactory.createRenderer(_document, renderType); + let styleEl = renderer.createElement('style'); - getDom().setAttribute(styleEl, 'type', 'text/css'); - if (!styleEl['styleSheet']) { - let cssText = `/* + renderer.setAttribute(styleEl, 'type', 'text/css'); + if (!styleEl['styleSheet']) { + let cssText = `/* @angular/flex-layout - workaround for possible browser quirk with mediaQuery listeners see http://bit.ly/2sd4HMP */ @media ${query} {.fx-query-test{ }}`; - getDom().appendChild(styleEl, getDom().createTextNode(cssText)); - } + renderer.appendChild(styleEl, renderer.createText(cssText)); + } - getDom().appendChild(_document.head, styleEl); + renderer.appendChild(_document.head, styleEl); - // Store in private global registry - list.forEach(mq => ALL_STYLES[mq] = styleEl); + // Store in private global registry + list.forEach(mq => ALL_STYLES[mq] = styleEl); - } catch (e) { - console.error(e); + } catch (e) { + console.error(e); + } } } } +/** + * Private global registry for all dynamically-created, injected style tags + * @see prepare(query) + */ +const ALL_STYLES = {}; + /** * Always convert to unique list of queries; for iteration in ::registerQuery() */ diff --git a/src/lib/media-query/mock/mock-match-media.ts b/src/lib/media-query/mock/mock-match-media.ts index acb8a4e9e..0eec97986 100644 --- a/src/lib/media-query/mock/mock-match-media.ts +++ b/src/lib/media-query/mock/mock-match-media.ts @@ -5,8 +5,8 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, NgZone} from '@angular/core'; -import {DOCUMENT} from '@angular/platform-browser'; +import {Inject, Injectable, NgZone, PLATFORM_ID, RendererFactory2} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; import {MatchMedia} from '../match-media'; import {BreakPointRegistry} from '../breakpoints/break-point-registry'; @@ -31,9 +31,11 @@ export class MockMatchMedia extends MatchMedia { useOverlaps = false; constructor(_zone: NgZone, + _rendererFactory: RendererFactory2, @Inject(DOCUMENT) _document: any, - private _breakpoints: BreakPointRegistry) { - super(_zone, _document); + private _breakpoints: BreakPointRegistry, + @Inject(PLATFORM_ID) _platformId: Object) { + super(_zone, _rendererFactory, _document, _platformId); this._actives = []; } diff --git a/src/lib/utils/style-utils.ts b/src/lib/utils/style-utils.ts index 744c66e34..608ff07f7 100644 --- a/src/lib/utils/style-utils.ts +++ b/src/lib/utils/style-utils.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ import {Renderer2} from '@angular/core'; -import {ɵgetDOM as getDom} from '@angular/platform-browser'; import {applyCssPrefixes} from './auto-prefixer'; +import {isPlatformBrowser} from '@angular/common'; /** * Definition of a css style. Either a property name (e.g. "flex-basis") or an object @@ -66,28 +66,29 @@ export function applyMultiValueStyleToElement(styles: {}, element: any, renderer * Find the DOM element's raw attribute value (if any) */ export function lookupAttributeValue(element: HTMLElement, attribute: string): string { - return getDom().getAttribute(element, attribute) || ''; + return element.getAttribute(attribute) || ''; } /** * Find the DOM element's inline style value (if any) */ export function lookupInlineStyle(element: HTMLElement, styleName: string): string { - return getDom().getStyle(element, styleName); + return element.style[styleName]; } /** * Determine the inline or inherited CSS style */ -export function lookupStyle(element: HTMLElement, styleName: string, inlineOnly = false): string { +export function lookupStyle(_platformId: Object, + element: HTMLElement, + styleName: string, + inlineOnly = false): string { let value = ''; if (element) { - try { - let immediateValue = value = lookupInlineStyle(element, styleName); - if ( !inlineOnly ) { - value = immediateValue || getDom().getComputedStyle(element).getPropertyValue(styleName); - } - } catch (e) { - // TODO: platform-server throws an exception for getComputedStyle, will be fixed by PR 18362 + let immediateValue = value = lookupInlineStyle(element, styleName); + if (!inlineOnly) { + // TODO(CaerusKaru): platform-server has no implementation for getComputedStyle + value = immediateValue || (isPlatformBrowser(_platformId) && + getComputedStyle(element).getPropertyValue(styleName)) || ''; } } diff --git a/src/universal-app/app/splitter/split.directive.ts b/src/universal-app/app/splitter/split.directive.ts index e4d766bdb..fecbc7e44 100644 --- a/src/universal-app/app/splitter/split.directive.ts +++ b/src/universal-app/app/splitter/split.directive.ts @@ -1,6 +1,14 @@ import { - Directive, Input, ContentChild, - ContentChildren, AfterContentInit, QueryList, ElementRef, OnDestroy + Directive, + Input, + ContentChild, + ContentChildren, + AfterContentInit, + QueryList, + ElementRef, + OnDestroy, + Inject, + PLATFORM_ID, } from '@angular/core'; import {SplitAreaDirective} from './split-area.directive'; @@ -8,7 +16,7 @@ import {SplitHandleDirective} from './split-handle.directive'; import {FlexDirective} from '@angular/flex-layout'; import {Subscription} from 'rxjs/Subscription'; -import { isBrowser } from '../util/helper'; +import {isPlatformBrowser} from '@angular/common'; @Directive({ selector: '[ngxSplit]', @@ -25,11 +33,11 @@ export class SplitDirective implements AfterContentInit, OnDestroy { @ContentChild(SplitHandleDirective) handle: SplitHandleDirective; @ContentChildren(SplitAreaDirective) areas: QueryList; - constructor(private elementRef: ElementRef) { + constructor(private elementRef: ElementRef, @Inject(PLATFORM_ID) private _platformId: Object) { } ngAfterContentInit(): void { - if (isBrowser()) { + if (isPlatformBrowser(this._platformId)) { this.watcher = this.handle.drag.subscribe(pos => this.onDrag(pos)); } } diff --git a/src/universal-app/app/util/helper.ts b/src/universal-app/app/util/helper.ts deleted file mode 100644 index e8e9c29ef..000000000 --- a/src/universal-app/app/util/helper.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {ɵgetDOM as getDom} from '@angular/platform-browser'; - -export function isBrowser() { - return getDom().supportsDOMEvents(); -} From 29f1c3d225e1e49c71d82a1351893af485a012a4 Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Mon, 15 Jan 2018 03:57:41 -0500 Subject: [PATCH 2/2] fix: add correct ssr styles * Add virtual stylesheet to store server styles, which applies default styles when breakpoint overrides are not present * Intercept all style calls and reroute them to the virtual stylesheet while not in the browser * Add a new type of MediaQueryList similar to the MockMediaQueryList for the server that allows for manual activation/deactivation of breakpoints * Update deps to Angular v5.2.x and TypeScript v2.6.x --- src/lib/api/core/base-adapter.spec.ts | 5 +- src/lib/api/core/base-adapter.ts | 6 +- src/lib/api/core/base.ts | 45 ++++++-- src/lib/api/ext/class.spec.ts | 4 +- src/lib/api/ext/class.ts | 13 ++- src/lib/api/ext/hide.spec.ts | 4 +- src/lib/api/ext/img-src.ts | 6 +- src/lib/api/ext/show-hide.ts | 6 +- src/lib/api/ext/show.spec.ts | 4 +- src/lib/api/ext/style.spec.ts | 4 +- src/lib/api/ext/style.ts | 15 ++- src/lib/api/flexbox/flex-align.ts | 6 +- src/lib/api/flexbox/flex-fill.ts | 6 +- src/lib/api/flexbox/flex-offset.ts | 6 +- src/lib/api/flexbox/flex-order.ts | 6 +- src/lib/api/flexbox/flex.ts | 6 +- src/lib/api/flexbox/layout-align.ts | 6 +- src/lib/api/flexbox/layout-gap.ts | 6 +- src/lib/api/flexbox/layout-wrap.ts | 17 +-- src/lib/api/flexbox/layout.ts | 6 +- src/lib/media-query/match-media.ts | 112 +++++++++++++----- src/lib/module.ts | 6 +- src/lib/utils/server-provider.ts | 146 ++++++++++++++++++++++++ src/lib/utils/server-stylesheet.ts | 60 ++++++++++ src/lib/utils/style-utils.ts | 4 +- src/universal-app/app/responsive-app.ts | 4 +- 26 files changed, 427 insertions(+), 82 deletions(-) create mode 100644 src/lib/utils/server-provider.ts create mode 100644 src/lib/utils/server-stylesheet.ts diff --git a/src/lib/api/core/base-adapter.spec.ts b/src/lib/api/core/base-adapter.spec.ts index c7fcdd425..053b7184a 100644 --- a/src/lib/api/core/base-adapter.spec.ts +++ b/src/lib/api/core/base-adapter.spec.ts @@ -8,7 +8,8 @@ import {ElementRef, Renderer2} from '@angular/core'; import {BaseFxDirectiveAdapter} from './base-adapter'; import {expect} from '../../utils/testing/custom-matchers'; -import {MediaMonitor} from '@angular/flex-layout/media-query'; +import {MediaMonitor} from '../../media-query/media-monitor'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; export class MockElementRef extends ElementRef { constructor() { @@ -21,7 +22,7 @@ export class MockElementRef extends ElementRef { describe('BaseFxDirectiveAdapter class', () => { let component; beforeEach(() => { - component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as Renderer2, {}); // tslint:disable-line:max-line-length + component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as Renderer2, {}, {} as ServerStylesheet); // tslint:disable-line:max-line-length }); describe('cacheInput', () => { it('should call _cacheInputArray when source is an array', () => { diff --git a/src/lib/api/core/base-adapter.ts b/src/lib/api/core/base-adapter.ts index 82ca37698..910765f59 100644 --- a/src/lib/api/core/base-adapter.ts +++ b/src/lib/api/core/base-adapter.ts @@ -11,6 +11,7 @@ import {BaseFxDirective} from './base'; import {ResponsiveActivation} from './responsive-activation'; import {MediaQuerySubscriber} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** @@ -49,8 +50,9 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, protected _renderer: Renderer2, - @Inject(PLATFORM_ID) protected _platformId: Object) { - super(_mediaMonitor, _elementRef, _renderer, _platformId); + @Inject(PLATFORM_ID) protected _platformId: Object, + protected _serverStylesheet: ServerStylesheet) { + super(_mediaMonitor, _elementRef, _renderer, _platformId, _serverStylesheet); } /** diff --git a/src/lib/api/core/base.ts b/src/lib/api/core/base.ts index 396403067..4b31ff34a 100644 --- a/src/lib/api/core/base.ts +++ b/src/lib/api/core/base.ts @@ -29,6 +29,8 @@ import { import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation'; import {MediaMonitor} from '../../media-query/media-monitor'; import {MediaQuerySubscriber} from '../../media-query/media-change'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; +import {isPlatformBrowser} from '@angular/common'; /** Abstract base class for the Layout API styling directives. */ export abstract class BaseFxDirective implements OnDestroy, OnChanges { @@ -71,7 +73,8 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { constructor(protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, protected _renderer: Renderer2, - @Inject(PLATFORM_ID) protected _platformId: Object) { + @Inject(PLATFORM_ID) protected _platformId: Object, + protected _serverStylesheet: ServerStylesheet) { } // ********************************************* @@ -137,11 +140,16 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { /** * Quick accessor to the current HTMLElement's `display` style - * Note: this allows use to preserve the original style + * Note: this allows us to preserve the original style * and optional restore it when the mediaQueries deactivate */ protected _getDisplayStyle(source: HTMLElement = this.nativeElement): string { - return lookupStyle(this._platformId, source || this.nativeElement, 'display'); + const query = 'display'; + if (isPlatformBrowser(this._platformId)) { + return lookupStyle(this._platformId, source || this.nativeElement, query); + } else { + return this._serverStylesheet.getStyleForElement(source, query); + } } /** @@ -160,13 +168,26 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ protected _getFlowDirection(target: any, addIfMissing = false): string { let value = 'row'; + let hasInlineValue = ''; + const query = 'flex-direction'; if (target) { - value = lookupStyle(this._platformId, target, 'flex-direction') || 'row'; - let hasInlineValue = lookupInlineStyle(target, 'flex-direction'); + if (isPlatformBrowser(this._platformId)) { + value = lookupStyle(this._platformId, target, query) || 'row'; + hasInlineValue = lookupInlineStyle(target, query); + } else { + // TODO(CaerusKaru): platform-server has no implementation for getComputedStyle + value = this._serverStylesheet.getStyleForElement(target, query) || 'row'; + } if (!hasInlineValue && addIfMissing) { - applyStyleToElements(this._renderer, buildLayoutCSS(value), [target]); + const style = buildLayoutCSS(value); + const elements = [target]; + if (isPlatformBrowser(this._platformId)) { + applyStyleToElements(this._renderer, style, elements); + } else { + this._serverStylesheet.addStyleToElements(style, elements); + } } } @@ -180,14 +201,22 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { value?: string | number, nativeElement: any = this.nativeElement) { let element = nativeElement || this.nativeElement; - applyStyleToElement(this._renderer, element, style, value); + if (isPlatformBrowser(this._platformId)) { + applyStyleToElement(this._renderer, element, style, value); + } else { + this._serverStylesheet.addStyleToElement(element, style, value); + } } /** * Applies styles given via string pair or object map to the directive's element. */ protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[ ]) { - applyStyleToElements(this._renderer, style, elements || []); + if (isPlatformBrowser(this._platformId)) { + applyStyleToElements(this._renderer, style, elements || []); + } else { + this._serverStylesheet.addStyleToElements(style, elements || []); + } } /** diff --git a/src/lib/api/ext/class.spec.ts b/src/lib/api/ext/class.spec.ts index c1268e74d..1528c4307 100644 --- a/src/lib/api/ext/class.spec.ts +++ b/src/lib/api/ext/class.spec.ts @@ -22,6 +22,7 @@ import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-regi import {ClassDirective} from './class'; import {MediaQueriesModule} from '../../media-query/_module'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; describe('class directive', () => { let fixture: ComponentFixture; @@ -47,7 +48,8 @@ describe('class directive', () => { declarations: [TestClassComponent, ClassDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet ] }); }); diff --git a/src/lib/api/ext/class.ts b/src/lib/api/ext/class.ts index c07636a14..75a020ed7 100644 --- a/src/lib/api/ext/class.ts +++ b/src/lib/api/ext/class.ts @@ -29,6 +29,7 @@ import {BaseFxDirectiveAdapter} from '../core/base-adapter'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {RendererAdapter} from '../core/renderer-adapter'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** NgClass allowed inputs **/ export type NgClassType = string | string[] | Set | {[klass: string]: any}; @@ -95,8 +96,9 @@ export class ClassDirective extends BaseFxDirective protected _ngEl: ElementRef, protected _renderer: Renderer2, @Optional() @Self() private _ngClassInstance: NgClass, - @Inject(PLATFORM_ID) protected _platformId: Object) { - super(monitor, _ngEl, _renderer, _platformId); + @Inject(PLATFORM_ID) protected _platformId: Object, + protected _serverStylesheet: ServerStylesheet) { + super(monitor, _ngEl, _renderer, _platformId, _serverStylesheet); this._configureAdapters(); } @@ -139,7 +141,12 @@ export class ClassDirective extends BaseFxDirective */ protected _configureAdapters() { this._base = new BaseFxDirectiveAdapter( - 'ngClass', this.monitor, this._ngEl, this._renderer, this._platformId + 'ngClass', + this.monitor, + this._ngEl, + this._renderer, + this._platformId, + this._serverStylesheet, ); if (!this._ngClassInstance) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been defined on diff --git a/src/lib/api/ext/hide.spec.ts b/src/lib/api/ext/hide.spec.ts index 44ad28ef1..2897d2fc2 100644 --- a/src/lib/api/ext/hide.spec.ts +++ b/src/lib/api/ext/hide.spec.ts @@ -23,6 +23,7 @@ import { } from '../../utils/testing/helpers'; import {ShowHideDirective} from './show-hide'; import {MediaQueriesModule} from '../../media-query/_module'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; describe('hide directive', () => { let fixture: ComponentFixture; @@ -60,7 +61,8 @@ describe('hide directive', () => { declarations: [TestHideComponent, ShowHideDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet ] }); }); diff --git a/src/lib/api/ext/img-src.ts b/src/lib/api/ext/img-src.ts index b821bb3b4..ea45ffa00 100644 --- a/src/lib/api/ext/img-src.ts +++ b/src/lib/api/ext/img-src.ts @@ -18,6 +18,7 @@ import { import {BaseFxDirective} from '../core/base'; import {MediaMonitor} from '../../media-query/media-monitor'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * This directive provides a responsive API for the HTML 'src' attribute @@ -60,8 +61,9 @@ export class ImgSrcDirective extends BaseFxDirective implements OnInit, OnChange constructor(elRef: ElementRef, renderer: Renderer2, monitor: MediaMonitor, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); this._cacheInput('src', elRef.nativeElement.getAttribute('src') || ''); } diff --git a/src/lib/api/ext/show-hide.ts b/src/lib/api/ext/show-hide.ts index ca9ea65f9..554707471 100644 --- a/src/lib/api/ext/show-hide.ts +++ b/src/lib/api/ext/show-hide.ts @@ -26,6 +26,7 @@ import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from '../flexbox/layout'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; const FALSY = ['false', false, 0]; @@ -107,9 +108,10 @@ export class ShowHideDirective extends BaseFxDirective implements OnInit, OnChan @Optional() @Self() protected _layout: LayoutDirective, protected elRef: ElementRef, protected renderer: Renderer2, - @Inject(PLATFORM_ID) protected platformId: Object) { + @Inject(PLATFORM_ID) protected platformId: Object, + protected serverStylesheet: ServerStylesheet) { - super(monitor, elRef, renderer, platformId); + super(monitor, elRef, renderer, platformId, serverStylesheet); if (_layout) { /** diff --git a/src/lib/api/ext/show.spec.ts b/src/lib/api/ext/show.spec.ts index 01162d8bc..4ee1175c7 100644 --- a/src/lib/api/ext/show.spec.ts +++ b/src/lib/api/ext/show.spec.ts @@ -18,6 +18,7 @@ import {FlexLayoutModule} from '../../module'; import {customMatchers} from '../../utils/testing/custom-matchers'; import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; describe('show directive', () => { let fixture: ComponentFixture; @@ -39,7 +40,8 @@ describe('show directive', () => { declarations: [TestShowComponent], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet ] }); }); diff --git a/src/lib/api/ext/style.spec.ts b/src/lib/api/ext/style.spec.ts index 421a4f017..302561b9a 100644 --- a/src/lib/api/ext/style.spec.ts +++ b/src/lib/api/ext/style.spec.ts @@ -22,6 +22,7 @@ import {customMatchers} from '../../utils/testing/custom-matchers'; import { makeCreateTestComponent, expectNativeEl } from '../../utils/testing/helpers'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; describe('style directive', () => { let fixture: ComponentFixture; @@ -43,7 +44,8 @@ describe('style directive', () => { declarations: [TestStyleComponent, LayoutDirective, StyleDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet ] }); }); diff --git a/src/lib/api/ext/style.ts b/src/lib/api/ext/style.ts index e9c36dd23..f5465fcae 100644 --- a/src/lib/api/ext/style.ts +++ b/src/lib/api/ext/style.ts @@ -38,6 +38,7 @@ import { ngStyleUtils as _ } from '../../utils/style-transforms'; import {RendererAdapter} from '../core/renderer-adapter'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** @@ -93,9 +94,10 @@ export class StyleDirective extends BaseFxDirective protected _renderer: Renderer2, protected _differs: KeyValueDiffers, @Optional() @Self() private _ngStyleInstance: NgStyle, - @Inject(PLATFORM_ID) protected _platformId: Object) { + @Inject(PLATFORM_ID) protected _platformId: Object, + protected _serverStylesheet: ServerStylesheet) { - super(monitor, _ngEl, _renderer, _platformId); + super(monitor, _ngEl, _renderer, _platformId, _serverStylesheet); this._configureAdapters(); } @@ -138,9 +140,14 @@ export class StyleDirective extends BaseFxDirective */ protected _configureAdapters() { this._base = new BaseFxDirectiveAdapter( - 'ngStyle', this.monitor, this._ngEl, this._renderer, this._platformId + 'ngStyle', + this.monitor, + this._ngEl, + this._renderer, + this._platformId, + this._serverStylesheet, ); - if ( !this._ngStyleInstance ) { + if (!this._ngStyleInstance) { // Create an instance NgClass Directive instance only if `ngClass=""` has NOT been // defined on the same host element; since the responsive variations may be defined... let adapter = new RendererAdapter(this._renderer); diff --git a/src/lib/api/flexbox/flex-align.ts b/src/lib/api/flexbox/flex-align.ts index ff4f30e69..db52aa762 100644 --- a/src/lib/api/flexbox/flex-align.ts +++ b/src/lib/api/flexbox/flex-align.ts @@ -21,6 +21,7 @@ import { import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * 'flex-align' flexbox styling directive @@ -59,8 +60,9 @@ export class FlexAlignDirective extends BaseFxDirective implements OnInit, OnCha constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); } diff --git a/src/lib/api/flexbox/flex-fill.ts b/src/lib/api/flexbox/flex-fill.ts index 8e73334df..a31ddf078 100644 --- a/src/lib/api/flexbox/flex-fill.ts +++ b/src/lib/api/flexbox/flex-fill.ts @@ -9,6 +9,7 @@ import {Directive, ElementRef, Inject, PLATFORM_ID, Renderer2} from '@angular/co import {MediaMonitor} from '../../media-query/media-monitor'; import {BaseFxDirective} from '../core/base'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; const FLEX_FILL_CSS = { 'margin': 0, @@ -32,8 +33,9 @@ export class FlexFillDirective extends BaseFxDirective { constructor(monitor: MediaMonitor, public elRef: ElementRef, public renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); this._applyStyleToElement(FLEX_FILL_CSS); } } diff --git a/src/lib/api/flexbox/flex-offset.ts b/src/lib/api/flexbox/flex-offset.ts index a97f8992e..6c77d96f0 100644 --- a/src/lib/api/flexbox/flex-offset.ts +++ b/src/lib/api/flexbox/flex-offset.ts @@ -27,6 +27,7 @@ import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from './layout'; import {isFlowHorizontal} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * 'flex-offset' flexbox styling directive @@ -63,8 +64,9 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh elRef: ElementRef, renderer: Renderer2, @Optional() @SkipSelf() protected _container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); this.watchParentFlow(); diff --git a/src/lib/api/flexbox/flex-order.ts b/src/lib/api/flexbox/flex-order.ts index 8c3953554..9f5294687 100644 --- a/src/lib/api/flexbox/flex-order.ts +++ b/src/lib/api/flexbox/flex-order.ts @@ -21,6 +21,7 @@ import { import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * 'flex-order' flexbox styling directive @@ -57,8 +58,9 @@ export class FlexOrderDirective extends BaseFxDirective implements OnInit, OnCha constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); } // ********************************************* diff --git a/src/lib/api/flexbox/flex.ts b/src/lib/api/flexbox/flex.ts index 1ad8cf475..e7d54f1a1 100644 --- a/src/lib/api/flexbox/flex.ts +++ b/src/lib/api/flexbox/flex.ts @@ -30,6 +30,7 @@ import {LayoutDirective} from './layout'; import {LayoutWrapDirective} from './layout-wrap'; import {validateBasis} from '../../utils/basis-validator'; import {isFlowHorizontal} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** Built-in aliases for different flex-basis values. */ @@ -89,9 +90,10 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, renderer: Renderer2, @Optional() @SkipSelf() protected _container: LayoutDirective, @Optional() @SkipSelf() protected _wrap: LayoutWrapDirective, - @Inject(PLATFORM_ID) platformId: Object) { + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { - super(monitor, elRef, renderer, platformId); + super(monitor, elRef, renderer, platformId, serverStylesheet); this._cacheInput('flex', ''); this._cacheInput('shrink', 1); diff --git a/src/lib/api/flexbox/layout-align.ts b/src/lib/api/flexbox/layout-align.ts index cd18aacc0..9c79aa61f 100644 --- a/src/lib/api/flexbox/layout-align.ts +++ b/src/lib/api/flexbox/layout-align.ts @@ -28,6 +28,7 @@ import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from './layout'; import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * 'layout-align' flexbox styling directive @@ -72,8 +73,9 @@ export class LayoutAlignDirective extends BaseFxDirective implements OnInit, OnC monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, @Optional() @Self() container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout-gap.ts b/src/lib/api/flexbox/layout-gap.ts index 7626ebe58..2824b75bd 100644 --- a/src/lib/api/flexbox/layout-gap.ts +++ b/src/lib/api/flexbox/layout-gap.ts @@ -27,6 +27,7 @@ import {LayoutDirective} from './layout'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LAYOUT_VALUES} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * 'layout-padding' styling directive @@ -70,8 +71,9 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI renderer: Renderer2, @Optional() @Self() container: LayoutDirective, private _zone: NgZone, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout-wrap.ts b/src/lib/api/flexbox/layout-wrap.ts index 991021a12..18622a334 100644 --- a/src/lib/api/flexbox/layout-wrap.ts +++ b/src/lib/api/flexbox/layout-wrap.ts @@ -26,6 +26,7 @@ import {LayoutDirective} from './layout'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {validateWrapValue, LAYOUT_VALUES} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; /** * @deprecated * This functionality is now part of the `fxLayout` API @@ -65,14 +66,14 @@ export class LayoutWrapDirective extends BaseFxDirective implements OnInit, OnCh @Input('fxLayoutWrap.lt-xl') set wrapLtXl(val) { this._cacheInput('wrapLtXl', val); }; /* tslint:enable */ - constructor( - monitor: MediaMonitor, - elRef: ElementRef, - renderer: Renderer2, - @Optional() @Self() container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { - - super(monitor, elRef, renderer, platformId); + constructor(monitor: MediaMonitor, + elRef: ElementRef, + renderer: Renderer2, + @Optional() @Self() container: LayoutDirective, + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + + super(monitor, elRef, renderer, platformId, serverStylesheet); if (container) { // Subscribe to layout direction changes this._layoutWatcher = container.layout$.subscribe(this._onLayoutChange.bind(this)); diff --git a/src/lib/api/flexbox/layout.ts b/src/lib/api/flexbox/layout.ts index 62d6d5ae5..4fe127ca8 100644 --- a/src/lib/api/flexbox/layout.ts +++ b/src/lib/api/flexbox/layout.ts @@ -23,6 +23,7 @@ import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {buildLayoutCSS} from '../../utils/layout-validator'; +import {ServerStylesheet} from '../../utils/server-stylesheet'; import {ReplaySubject} from 'rxjs/ReplaySubject'; /** * 'layout' flexbox styling directive @@ -76,8 +77,9 @@ export class LayoutDirective extends BaseFxDirective implements OnInit, OnChange constructor(monitor: MediaMonitor, elRef: ElementRef, renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); + @Inject(PLATFORM_ID) platformId: Object, + serverStylesheet: ServerStylesheet) { + super(monitor, elRef, renderer, platformId, serverStylesheet); this._announcer = new ReplaySubject(1); this.layout$ = this._announcer.asObservable(); } diff --git a/src/lib/media-query/match-media.ts b/src/lib/media-query/match-media.ts index 248b652dd..01577c2ab 100644 --- a/src/lib/media-query/match-media.ts +++ b/src/lib/media-query/match-media.ts @@ -20,27 +20,76 @@ import {Observable} from 'rxjs/Observable'; import {filter} from 'rxjs/operators/filter'; import {MediaChange} from './media-change'; +import {BreakPoint} from '../media-query/breakpoints/break-point'; /** - * EventHandler callback with the mediaQuery [range] activates or deactivates + * Special server-only class to simulate a MediaQueryList and + * - supports manual activation to simulate mediaQuery matching + * - manages listeners */ -export interface MediaQueryListListener { - // Function with Window's MediaQueryList argument - (mql: MediaQueryList): void; -} +export class ServerMediaQueryList implements MediaQueryList { + private _isActive = false; + private _listeners: Array = []; -/** - * EventDispatcher for a specific mediaQuery [range] - */ -export interface MediaQueryList { - readonly matches: boolean; - readonly media: string; + get matches(): boolean { + return this._isActive; + } - addListener(listener: MediaQueryListListener): void; + get media(): string { + return this._mediaQuery; + } - removeListener(listener: MediaQueryListListener): void; -} + constructor(private _mediaQuery: string) { } + + /** + * + */ + destroy() { + this.deactivate(); + this._listeners = []; + } + /** + * Notify all listeners that 'matches === TRUE' + */ + activate(): ServerMediaQueryList { + if (!this._isActive) { + this._isActive = true; + this._listeners.forEach((callback) => { + callback(this); + }); + } + return this; + } + + /** + * Notify all listeners that 'matches === false' + */ + deactivate(): ServerMediaQueryList { + if (this._isActive) { + this._isActive = false; + this._listeners.forEach((callback) => { + callback(this); + }); + } + return this; + } + + /** + * + */ + addListener(listener: MediaQueryListListener) { + if (this._listeners.indexOf(listener) === -1) { + this._listeners.push(listener); + } + if (this._isActive) { + listener(this); + } + } + + removeListener(_: MediaQueryListListener) { + } +} /** * MediaMonitor configures listeners to mediaQuery changes and publishes an Observable facade to @@ -51,7 +100,7 @@ export interface MediaQueryList { */ @Injectable() export class MatchMedia { - protected _registry: Map; + protected _registry: Map; protected _source: BehaviorSubject; protected _observable$: Observable; @@ -59,11 +108,29 @@ export class MatchMedia { protected _rendererFactory: RendererFactory2, @Inject(DOCUMENT) protected _document: any, @Inject(PLATFORM_ID) protected _platformId: Object) { - this._registry = new Map(); + this._registry = new Map(); this._source = new BehaviorSubject(new MediaChange(true)); this._observable$ = this._source.asObservable(); } + /** + * Activate the specified breakpoint if we're on the server, no-op otherwise + */ + activateBreakpoint(bp: BreakPoint) { + if (!isPlatformBrowser(this._platformId)) { + (this._registry.get(bp.mediaQuery) as ServerMediaQueryList).activate(); + } + } + + /** + * Deactivate the specified breakpoint if we're on the server, no-op otherwise + */ + deactivateBreakpoint(bp: BreakPoint) { + if (!isPlatformBrowser(this._platformId)) { + (this._registry.get(bp.mediaQuery) as ServerMediaQueryList).deactivate(); + } + } + /** * For the specified mediaQuery? */ @@ -104,7 +171,7 @@ export class MatchMedia { list.forEach(query => { let mql = this._registry.get(query); - let onMQLEvent = (e: MediaQueryList) => { + let onMQLEvent = (e: MediaQueryList|ServerMediaQueryList) => { this._zone.run(() => { let change = new MediaChange(e.matches, query); this._source.next(change); @@ -128,18 +195,11 @@ export class MatchMedia { * Call window.matchMedia() to build a MediaQueryList; which * supports 0..n listeners for activation/deactivation */ - protected _buildMQL(query: string): MediaQueryList { + protected _buildMQL(query: string): MediaQueryList|ServerMediaQueryList { let canListen = isPlatformBrowser(this._platformId) && !!(window).matchMedia('all').addListener; - return canListen ? (window).matchMedia(query) : { - matches: query === 'all' || query === '', - media: query, - addListener: () => { - }, - removeListener: () => { - } - }; + return canListen ? (window).matchMedia(query) : new ServerMediaQueryList(query); } /** diff --git a/src/lib/module.ts b/src/lib/module.ts index 8bfd13fd8..20333040e 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -32,6 +32,8 @@ import {ShowHideDirective} from './api/ext/show-hide'; import {ClassDirective} from './api/ext/class'; import {StyleDirective} from './api/ext/style'; import {ImgSrcDirective} from './api/ext/img-src'; +import {ServerStylesheet} from './utils/server-stylesheet'; +import {SERVER_PROVIDER} from './utils/server-provider'; /** * Since the equivalent results are easily achieved with a css class attached to each @@ -67,7 +69,9 @@ const ALL_DIRECTIVES = [ providers: [ MEDIA_MONITOR_PROVIDER, DEFAULT_BREAKPOINTS_PROVIDER, // Extend defaults with internal custom breakpoints - OBSERVABLE_MEDIA_PROVIDER + OBSERVABLE_MEDIA_PROVIDER, + ServerStylesheet, + SERVER_PROVIDER, ] }) export class FlexLayoutModule { diff --git a/src/lib/utils/server-provider.ts b/src/lib/utils/server-provider.ts new file mode 100644 index 000000000..bdf6345b4 --- /dev/null +++ b/src/lib/utils/server-provider.ts @@ -0,0 +1,146 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import { + APP_BOOTSTRAP_LISTENER, + PLATFORM_ID, + RendererFactory2, + RendererType2, + InjectionToken, // tslint:disable-line:no-unused-variable + ComponentRef, // tslint:disable-line:no-unused-variable + ViewEncapsulation, + Renderer2, +} from '@angular/core'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; +import {ServerStylesheet} from './server-stylesheet'; +import {BREAKPOINTS} from '../media-query/breakpoints/break-points-token'; +import {BreakPoint} from '../media-query/breakpoints/break-point'; +import {MatchMedia} from '../media-query/match-media'; + +const CLASS_NAME = 'flex-layout-'; +let UNIQUE_CLASS = 0; + +/** + * create @media queries based on a virtual stylesheet + * * Adds a unique class to each element and stores it + * in a shared classMap for later reuse + */ +function formatStyle(stylesheet: Map>, + renderer: Renderer2, + mediaQuery: string, + classMap: Map) { + let styleText = ` + @media ${mediaQuery} {`; + stylesheet.forEach((styles, el) => { + let className = classMap.get(el); + if (!className) { + className = `${CLASS_NAME}${UNIQUE_CLASS++}`; + classMap.set(el, className); + } + renderer.addClass(el, className); + styleText += ` + .${className} {`; + styles.forEach((v, k) => { + if (v) { + styleText += ` + ${k}: ${v};`; + } + }); + styleText += ` + }`; + }); + styleText += ` + }\n`; + + return styleText; +} + +/** + * format the static @media queries for all breakpoints + * to be used on the server and append them to the + */ +function serverStyles(renderer: Renderer2, + serverSheet: ServerStylesheet, + breakpoints: BreakPoint[], + matchMedia: MatchMedia, + _document: any) { + const styleTag = renderer.createElement('style'); + const classMap = new Map(); + const defaultStyles = new Map(serverSheet.stylesheet); + let styleText = formatStyle(defaultStyles, renderer, 'all', classMap); + + breakpoints.reverse(); + breakpoints.forEach((bp, i) => { + serverSheet.clearStyles(); + + if (i > 0) { + matchMedia.deactivateBreakpoint(breakpoints[i - 1]); + } + + matchMedia.activateBreakpoint(bp); + const stylesheet = new Map(serverSheet.stylesheet); + if (stylesheet.size > 0) { + styleText += formatStyle(stylesheet, renderer, bp.mediaQuery, classMap); + } + }); + + renderer.addClass(styleTag, `${CLASS_NAME}ssr`); + renderer.setValue(styleTag, styleText); + renderer.appendChild(_document.head, styleTag); +} + +/** + * Add or remove static styles depending on the current + * platform + */ +export function addStyles(serverSheet: ServerStylesheet, + matchMedia: MatchMedia, + _document: Document, + rendererFactory: RendererFactory2, + platformId: Object, + breakpoints: BreakPoint[]) { + // necessary because of angular/angular/issues/14485 + const res = () => { + const renderType: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} + }; + const renderer = rendererFactory.createRenderer(_document, renderType); + if (!isPlatformBrowser(platformId)) { + serverStyles(renderer, serverSheet, breakpoints, matchMedia, _document); + } else { + const elements = Array.from(_document.querySelectorAll(`[class*=${CLASS_NAME}]`)); + const classRegex = new RegExp(/\bflex-layout-.+?\b/, 'g'); + elements.forEach(el => { + el.classList.contains(`${CLASS_NAME}ssr`) ? + el.remove() : el.className.replace(classRegex, ''); + }); + } + }; + + return res; +} + +/** + * Provider to set static styles on the server and remove + * them on the browser + */ +export const SERVER_PROVIDER = { + provide: APP_BOOTSTRAP_LISTENER, + useFactory: addStyles, + deps: [ + ServerStylesheet, + MatchMedia, + DOCUMENT, + RendererFactory2, + PLATFORM_ID, + BREAKPOINTS, + ], + multi: true +}; diff --git a/src/lib/utils/server-stylesheet.ts b/src/lib/utils/server-stylesheet.ts new file mode 100644 index 000000000..7f168fee3 --- /dev/null +++ b/src/lib/utils/server-stylesheet.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {Injectable} from '@angular/core'; +import {StyleDefinition} from './style-utils'; +import {applyCssPrefixes} from './auto-prefixer'; + +@Injectable() +export class ServerStylesheet { + + readonly stylesheet = new Map>(); + + constructor() { } + + addStyleToElement(element: any, style: StyleDefinition, value?: string | number) { + let styles = {}; + if (typeof style === 'string') { + styles[style] = value; + style = styles; + } + + styles = applyCssPrefixes(style); + this._applyMultiValueStyleToElement(styles, element); + } + + addStyleToElements(style: StyleDefinition, elements: HTMLElement[]) { + const styles = applyCssPrefixes(style); + elements.forEach(el => { + this._applyMultiValueStyleToElement(styles, el); + }); + } + + clearStyles() { + this.stylesheet.clear(); + } + + getStyleForElement(el: HTMLElement, styleName: string): string { + const styles = this.stylesheet.get(el); + return styles ? (styles.get(styleName) || '') : ''; + } + + private _applyMultiValueStyleToElement(styles: {}, element: any) { + Object.keys(styles).sort().forEach(key => { + const values = Array.isArray(styles[key]) ? styles[key] : [styles[key]]; + values.sort(); + for (let value of values) { + const stylesheet = this.stylesheet.get(element); + if (stylesheet) { + stylesheet.set(key, value); + } else { + this.stylesheet.set(element, new Map([[key, value]])); + } + } + }); + } +} diff --git a/src/lib/utils/style-utils.ts b/src/lib/utils/style-utils.ts index 608ff07f7..0613c9928 100644 --- a/src/lib/utils/style-utils.ts +++ b/src/lib/utils/style-utils.ts @@ -39,8 +39,8 @@ export function applyStyleToElement(renderer: Renderer2, */ export function applyStyleToElements(renderer: Renderer2, style: StyleDefinition, - elements: HTMLElement[ ]) { - let styles = applyCssPrefixes(style); + elements: HTMLElement[]) { + const styles = applyCssPrefixes(style); elements.forEach(el => { applyMultiValueStyleToElement(styles, el, renderer); diff --git a/src/universal-app/app/responsive-app.ts b/src/universal-app/app/responsive-app.ts index 044d43809..edabe0867 100644 --- a/src/universal-app/app/responsive-app.ts +++ b/src/universal-app/app/responsive-app.ts @@ -11,7 +11,7 @@ import {SplitModule} from './splitter/split.module'; styleUrls: ['./responsive-app.css'], template: `
-
+
Column #1 - Row #1
    @@ -23,7 +23,7 @@ import {SplitModule} from './splitter/split.module';
    -
    +
    Column #2 - Row #1