From 305ab8feb219b918e2cb146a588fa7d74327325a Mon Sep 17 00:00:00 2001 From: Adam Plumer Date: Mon, 15 Jan 2018 03:57:41 -0500 Subject: [PATCH] feat(ssr): enhance support for Universal and SSR with stylesheets * Add StylerService class to manage application and retrieval of styles from elements in a platform-agnostic manner * 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 Fixes #373. > See [Design Doc](https://docs.google.com/document/d/1fg04ihw42dJJHGd6fugdiBe39iJot8aErhiE7CjwfmQ/edit#) --- src/lib/api/core/base-adapter.spec.ts | 15 +- src/lib/api/core/base-adapter.ts | 17 +- src/lib/api/core/base.ts | 75 +++++--- src/lib/api/ext/class.spec.ts | 15 +- src/lib/api/ext/class.ts | 25 ++- src/lib/api/ext/hide.spec.ts | 15 +- src/lib/api/ext/img-src.ts | 24 ++- src/lib/api/ext/show-hide.ts | 25 ++- src/lib/api/ext/show.spec.ts | 15 +- src/lib/api/ext/style.spec.ts | 15 +- src/lib/api/ext/style.ts | 30 ++- src/lib/api/flexbox/flex-align.ts | 17 +- src/lib/api/flexbox/flex-fill.ts | 16 +- src/lib/api/flexbox/flex-offset.ts | 17 +- src/lib/api/flexbox/flex-order.ts | 17 +- src/lib/api/flexbox/flex.ts | 17 +- src/lib/api/flexbox/layout-align.ts | 18 +- src/lib/api/flexbox/layout-gap.ts | 17 +- src/lib/api/flexbox/layout.ts | 17 +- src/lib/media-query/match-media.ts | 112 ++++++++--- src/lib/module.ts | 16 +- src/lib/utils/index.ts | 2 +- src/lib/utils/server-provider.ts | 147 +++++++++++++++ src/lib/utils/server-stylesheet.ts | 60 ++++++ src/lib/utils/style-utils.ts | 178 ++++++++++-------- src/lib/utils/styling/index.ts | 12 ++ src/lib/utils/styling/server-provider.ts | 147 +++++++++++++++ src/lib/utils/styling/server-stylesheet.ts | 60 ++++++ .../{ => styling}/style-transforms.spec.ts | 2 +- .../utils/{ => styling}/style-transforms.ts | 0 .../styler.spec.ts} | 4 +- src/lib/utils/styling/styler.ts | 129 +++++++++++++ src/universal-app/app/responsive-app.ts | 4 +- 33 files changed, 1064 insertions(+), 216 deletions(-) create mode 100644 src/lib/utils/server-provider.ts create mode 100644 src/lib/utils/server-stylesheet.ts create mode 100644 src/lib/utils/styling/index.ts create mode 100644 src/lib/utils/styling/server-provider.ts create mode 100644 src/lib/utils/styling/server-stylesheet.ts rename src/lib/utils/{ => styling}/style-transforms.spec.ts (96%) rename src/lib/utils/{ => styling}/style-transforms.ts (100%) rename src/lib/utils/{style-utils.spec.ts => styling/styler.spec.ts} (94%) create mode 100644 src/lib/utils/styling/styler.ts diff --git a/src/lib/api/core/base-adapter.spec.ts b/src/lib/api/core/base-adapter.spec.ts index c7fcdd425..b7366383e 100644 --- a/src/lib/api/core/base-adapter.spec.ts +++ b/src/lib/api/core/base-adapter.spec.ts @@ -5,10 +5,15 @@ * 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} 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'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes export class MockElementRef extends ElementRef { constructor() { @@ -21,7 +26,11 @@ 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 +<<<<<<< Updated upstream + component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as StyleUtils); // tslint:disable-line:max-line-length +======= + component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as StylerService); // tslint:disable-line:max-line-length +>>>>>>> Stashed changes }); 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 c9b240560..d288e86a1 100644 --- a/src/lib/api/core/base-adapter.ts +++ b/src/lib/api/core/base-adapter.ts @@ -5,12 +5,17 @@ * 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, Inject, PLATFORM_ID, Renderer2} from '@angular/core'; +import {ElementRef} from '@angular/core'; import {BaseFxDirective} from './base'; import {ResponsiveActivation} from './responsive-activation'; import {MediaQuerySubscriber} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** @@ -48,9 +53,13 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective { constructor(protected _baseKey: string, // non-responsive @Input property name protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, - protected _renderer: Renderer2, - @Inject(PLATFORM_ID) protected _platformId: Object) { - super(_mediaMonitor, _elementRef, _renderer, _platformId); +<<<<<<< Updated upstream + protected _styleUtils: StyleUtils) { + super(_mediaMonitor, _elementRef, _styleUtils); +======= + protected _styler: StylerService) { + super(_mediaMonitor, _elementRef, _styler); +>>>>>>> Stashed changes } /** diff --git a/src/lib/api/core/base.ts b/src/lib/api/core/base.ts index 256ede987..919912e24 100644 --- a/src/lib/api/core/base.ts +++ b/src/lib/api/core/base.ts @@ -11,20 +11,18 @@ import { SimpleChanges, OnChanges, SimpleChange, - Renderer2, - Inject, - PLATFORM_ID, } from '@angular/core'; import {buildLayoutCSS} from '../../utils/layout-validator'; import { StyleDefinition, - lookupStyle, - lookupInlineStyle, - applyStyleToElement, - applyStyleToElements, - lookupAttributeValue, +<<<<<<< Updated upstream + StyleUtils, } from '../../utils/style-utils'; +======= + StylerService, +} from '../../utils/styling/styler'; +>>>>>>> Stashed changes import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation'; import {MediaMonitor} from '../../media-query/media-monitor'; @@ -70,8 +68,11 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ constructor(protected _mediaMonitor: MediaMonitor, protected _elementRef: ElementRef, - protected _renderer: Renderer2, - @Inject(PLATFORM_ID) protected _platformId: Object) { +<<<<<<< Updated upstream + protected _styleUtils: StyleUtils) { +======= + protected _styler: StylerService) { +>>>>>>> Stashed changes } // ********************************************* @@ -137,11 +138,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'; +<<<<<<< Updated upstream + return this._styleUtils.lookupStyle(source, query); +======= + return this._styler.lookupStyle(source, query); +>>>>>>> Stashed changes } /** @@ -149,7 +155,11 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ protected _getAttributeValue(attribute: string, source: HTMLElement = this.nativeElement): string { - return lookupAttributeValue(source || this.nativeElement, attribute); +<<<<<<< Updated upstream + return this._styleUtils.lookupAttributeValue(source, attribute); +======= + return this._styler.lookupAttributeValue(source, attribute); +>>>>>>> Stashed changes } /** @@ -158,15 +168,29 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { * Check inline style first then check computed (stylesheet) style. * And optionally add the flow value to element's inline style. */ - protected _getFlowDirection(target: any, addIfMissing = false): string { + protected _getFlowDirection(target: HTMLElement, 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'); + +<<<<<<< Updated upstream + value = this._styleUtils.lookupStyle(target, query) || 'row'; + hasInlineValue = this._styleUtils.lookupInlineStyle(target, query); +======= + value = this._styler.lookupStyle(target, query) || 'row'; + hasInlineValue = this._styler.lookupInlineStyle(target, query); +>>>>>>> Stashed changes if (!hasInlineValue && addIfMissing) { - applyStyleToElements(this._renderer, buildLayoutCSS(value), [target]); + const style = buildLayoutCSS(value); + const elements = [target]; +<<<<<<< Updated upstream + this._styleUtils.applyStyleToElements(style, elements); +======= + this._styler.applyStyleToElements(style, elements); +>>>>>>> Stashed changes } } @@ -178,16 +202,23 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges { */ protected _applyStyleToElement(style: StyleDefinition, value?: string | number, - nativeElement: any = this.nativeElement) { - let element = nativeElement || this.nativeElement; - applyStyleToElement(this._renderer, element, style, value); + element: HTMLElement = this.nativeElement) { +<<<<<<< Updated upstream + this._styleUtils.applyStyleToElement(element, style, value); +======= + this._styler.applyStyleToElement(element, style, value); +>>>>>>> Stashed changes } /** * 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 || []); + protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) { +<<<<<<< Updated upstream + this._styleUtils.applyStyleToElements(style, elements); +======= + this._styler.applyStyleToElements(style, elements); +>>>>>>> Stashed changes } /** diff --git a/src/lib/api/ext/class.spec.ts b/src/lib/api/ext/class.spec.ts index ed4dc6b09..3d5fb3e32 100644 --- a/src/lib/api/ext/class.spec.ts +++ b/src/lib/api/ext/class.spec.ts @@ -21,6 +21,13 @@ import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-regi import {ClassDirective} from './class'; import {MediaQueriesModule} from '../../media-query/_module'; +<<<<<<< Updated upstream +import {ServerStylesheet} from '../../utils/server-stylesheet'; +import {StyleUtils} from '../../utils/style-utils'; +======= +import {ServerStylesheet} from '../../utils/styling/server-stylesheet'; +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes describe('class directive', () => { let fixture: ComponentFixture; @@ -46,7 +53,13 @@ describe('class directive', () => { declarations: [TestClassComponent, ClassDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet, +<<<<<<< Updated upstream + StyleUtils, +======= + StylerService, +>>>>>>> Stashed changes ] }); }); diff --git a/src/lib/api/ext/class.ts b/src/lib/api/ext/class.ts index a1401c478..e3675c5ef 100644 --- a/src/lib/api/ext/class.ts +++ b/src/lib/api/ext/class.ts @@ -19,8 +19,6 @@ import { SimpleChanges, Self, OnInit, - Inject, - PLATFORM_ID, } from '@angular/core'; import {NgClass} from '@angular/common'; @@ -29,6 +27,11 @@ 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'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** NgClass allowed inputs **/ export type NgClassType = string | string[] | Set | {[klass: string]: any}; @@ -95,8 +98,13 @@ 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); +<<<<<<< Updated upstream + protected _styleUtils: StyleUtils) { + super(monitor, _ngEl, _styleUtils); +======= + protected _styler: StylerService) { + super(monitor, _ngEl, _styler); +>>>>>>> Stashed changes this._configureAdapters(); } @@ -139,7 +147,14 @@ 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, +<<<<<<< Updated upstream + this._styleUtils +======= + this._styler +>>>>>>> Stashed changes ); 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 69303cd9c..38df55483 100644 --- a/src/lib/api/ext/hide.spec.ts +++ b/src/lib/api/ext/hide.spec.ts @@ -23,6 +23,13 @@ import { } from '../../utils/testing/helpers'; import {ShowHideDirective} from './show-hide'; import {MediaQueriesModule} from '../../media-query/_module'; +<<<<<<< Updated upstream +import {ServerStylesheet} from '../../utils/server-stylesheet'; +import {StyleUtils} from '../../utils/style-utils'; +======= +import {ServerStylesheet} from '../../utils/styling/server-stylesheet'; +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes describe('hide directive', () => { let fixture: ComponentFixture; @@ -60,7 +67,13 @@ describe('hide directive', () => { declarations: [TestHideComponent, ShowHideDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet, +<<<<<<< Updated upstream + StyleUtils, +======= + StylerService, +>>>>>>> Stashed changes ] }); }); diff --git a/src/lib/api/ext/img-src.ts b/src/lib/api/ext/img-src.ts index b821bb3b4..8fa1fbeda 100644 --- a/src/lib/api/ext/img-src.ts +++ b/src/lib/api/ext/img-src.ts @@ -12,12 +12,15 @@ import { OnInit, OnChanges, Renderer2, - Inject, - PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; import {MediaMonitor} from '../../media-query/media-monitor'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * This directive provides a responsive API for the HTML 'src' attribute @@ -57,12 +60,17 @@ 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, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); - this._cacheInput('src', elRef.nativeElement.getAttribute('src') || ''); + constructor(protected _elRef: ElementRef, + protected _renderer: Renderer2, + protected _monitor: MediaMonitor, +<<<<<<< Updated upstream + protected _styleUtils: StyleUtils) { + super(_monitor, _elRef, _styleUtils); +======= + protected _styler: StylerService) { + super(_monitor, _elRef, _styler); +>>>>>>> Stashed changes + 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..4aba29def 100644 --- a/src/lib/api/ext/show-hide.ts +++ b/src/lib/api/ext/show-hide.ts @@ -12,12 +12,9 @@ import { OnInit, OnChanges, OnDestroy, - Renderer2, SimpleChanges, Self, Optional, - Inject, - PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -26,6 +23,11 @@ import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from '../flexbox/layout'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes const FALSY = ['false', false, 0]; @@ -104,19 +106,22 @@ export class ShowHideDirective extends BaseFxDirective implements OnInit, OnChan * */ constructor(monitor: MediaMonitor, - @Optional() @Self() protected _layout: LayoutDirective, + @Optional() @Self() protected layout: LayoutDirective, protected elRef: ElementRef, - protected renderer: Renderer2, - @Inject(PLATFORM_ID) protected platformId: Object) { +<<<<<<< Updated upstream + protected styleUtils: StyleUtils) { +======= + protected styleUtils: StylerService) { +>>>>>>> Stashed changes - super(monitor, elRef, renderer, platformId); + super(monitor, elRef, styleUtils); - if (_layout) { + if (layout) { /** * The Layout can set the display:flex (and incorrectly affect the Hide/Show directives. * Whenever Layout [on the same element] resets its CSS, then update the Hide/Show CSS */ - this._layoutWatcher = _layout.layout$.subscribe(() => this._updateWithValue()); + this._layoutWatcher = layout.layout$.subscribe(() => this._updateWithValue()); } } @@ -130,7 +135,7 @@ export class ShowHideDirective extends BaseFxDirective implements OnInit, OnChan * unless it was already explicitly specified inline or in a CSS stylesheet. */ protected _getDisplayStyle(): string { - return this._layout ? 'flex' : super._getDisplayStyle(); + return this.layout ? 'flex' : super._getDisplayStyle(); } diff --git a/src/lib/api/ext/show.spec.ts b/src/lib/api/ext/show.spec.ts index e87ea2015..c55fc1dc0 100644 --- a/src/lib/api/ext/show.spec.ts +++ b/src/lib/api/ext/show.spec.ts @@ -18,6 +18,13 @@ import {FlexLayoutModule} from '../../module'; import {customMatchers} from '../../utils/testing/custom-matchers'; import {makeCreateTestComponent, expectNativeEl} from '../../utils/testing/helpers'; +<<<<<<< Updated upstream +import {ServerStylesheet} from '../../utils/server-stylesheet'; +import {StyleUtils} from '../../utils/style-utils'; +======= +import {ServerStylesheet} from '../../utils/styling/server-stylesheet'; +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes describe('show directive', () => { let fixture: ComponentFixture; @@ -39,7 +46,13 @@ describe('show directive', () => { declarations: [TestShowComponent], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet, +<<<<<<< Updated upstream + StyleUtils, +======= + StylerService, +>>>>>>> Stashed changes ] }); }); diff --git a/src/lib/api/ext/style.spec.ts b/src/lib/api/ext/style.spec.ts index dc595d330..bd2a27e1a 100644 --- a/src/lib/api/ext/style.spec.ts +++ b/src/lib/api/ext/style.spec.ts @@ -22,6 +22,13 @@ import {customMatchers} from '../../utils/testing/custom-matchers'; import { makeCreateTestComponent, expectNativeEl } from '../../utils/testing/helpers'; +<<<<<<< Updated upstream +import {ServerStylesheet} from '../../utils/server-stylesheet'; +import {StyleUtils} from '../../utils/style-utils'; +======= +import {ServerStylesheet} from '../../utils/styling/server-stylesheet'; +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes describe('style directive', () => { let fixture: ComponentFixture; @@ -43,7 +50,13 @@ describe('style directive', () => { declarations: [TestStyleComponent, LayoutDirective, StyleDirective], providers: [ BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER, - {provide: MatchMedia, useClass: MockMatchMedia} + {provide: MatchMedia, useClass: MockMatchMedia}, + ServerStylesheet, +<<<<<<< Updated upstream + StyleUtils, +======= + StylerService, +>>>>>>> Stashed changes ] }); }); diff --git a/src/lib/api/ext/style.ts b/src/lib/api/ext/style.ts index 56b295e84..acd131159 100644 --- a/src/lib/api/ext/style.ts +++ b/src/lib/api/ext/style.ts @@ -19,8 +19,6 @@ import { Self, SimpleChanges, OnInit, - Inject, - PLATFORM_ID, } from '@angular/core'; import {NgStyle} from '@angular/common'; @@ -36,8 +34,13 @@ import { NgStyleType, NgStyleSanitizer, ngStyleUtils as _ -} from '../../utils/style-transforms'; +} from '../../utils/styling/style-transforms'; import {RendererAdapter} from '../core/renderer-adapter'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** @@ -93,9 +96,15 @@ export class StyleDirective extends BaseFxDirective protected _renderer: Renderer2, protected _differs: KeyValueDiffers, @Optional() @Self() private _ngStyleInstance: NgStyle, - @Inject(PLATFORM_ID) protected _platformId: Object) { +<<<<<<< Updated upstream + protected _styleUtils: StyleUtils) { - super(monitor, _ngEl, _renderer, _platformId); + super(monitor, _ngEl, _styleUtils); +======= + protected _styler: StylerService) { + + super(monitor, _ngEl, _styler); +>>>>>>> Stashed changes this._configureAdapters(); } @@ -138,9 +147,16 @@ 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, +<<<<<<< Updated upstream + this._styleUtils +======= + this._styler +>>>>>>> Stashed changes ); - 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..1e4d9aaa6 100644 --- a/src/lib/api/flexbox/flex-align.ts +++ b/src/lib/api/flexbox/flex-align.ts @@ -12,15 +12,17 @@ import { OnInit, OnChanges, OnDestroy, - Renderer2, SimpleChanges, - Inject, - PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'flex-align' flexbox styling directive @@ -58,9 +60,12 @@ export class FlexAlignDirective extends BaseFxDirective implements OnInit, OnCha /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); } diff --git a/src/lib/api/flexbox/flex-fill.ts b/src/lib/api/flexbox/flex-fill.ts index 8e73334df..38caeac76 100644 --- a/src/lib/api/flexbox/flex-fill.ts +++ b/src/lib/api/flexbox/flex-fill.ts @@ -5,10 +5,15 @@ * 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, Inject, PLATFORM_ID, Renderer2} from '@angular/core'; +import {Directive, ElementRef} from '@angular/core'; import {MediaMonitor} from '../../media-query/media-monitor'; import {BaseFxDirective} from '../core/base'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes const FLEX_FILL_CSS = { 'margin': 0, @@ -31,9 +36,12 @@ const FLEX_FILL_CSS = { export class FlexFillDirective extends BaseFxDirective { constructor(monitor: MediaMonitor, public elRef: ElementRef, - public renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); 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..a3eac0205 100644 --- a/src/lib/api/flexbox/flex-offset.ts +++ b/src/lib/api/flexbox/flex-offset.ts @@ -13,11 +13,8 @@ import { OnChanges, OnDestroy, Optional, - Renderer2, SimpleChanges, SkipSelf, - Inject, - PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -27,6 +24,11 @@ import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from './layout'; import {isFlowHorizontal} from '../../utils/layout-validator'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'flex-offset' flexbox styling directive @@ -61,10 +63,13 @@ export class FlexOffsetDirective extends BaseFxDirective implements OnInit, OnCh /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - renderer: Renderer2, @Optional() @SkipSelf() protected _container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); this.watchParentFlow(); diff --git a/src/lib/api/flexbox/flex-order.ts b/src/lib/api/flexbox/flex-order.ts index 8c3953554..f2712904f 100644 --- a/src/lib/api/flexbox/flex-order.ts +++ b/src/lib/api/flexbox/flex-order.ts @@ -12,15 +12,17 @@ import { OnInit, OnChanges, OnDestroy, - Renderer2, SimpleChanges, - Inject, - PLATFORM_ID, } from '@angular/core'; import {BaseFxDirective} from '../core/base'; import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'flex-order' flexbox styling directive @@ -56,9 +58,12 @@ export class FlexOrderDirective extends BaseFxDirective implements OnInit, OnCha /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - renderer: Renderer2, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); } // ********************************************* diff --git a/src/lib/api/flexbox/flex.ts b/src/lib/api/flexbox/flex.ts index a502a0bf2..b13fa9a05 100644 --- a/src/lib/api/flexbox/flex.ts +++ b/src/lib/api/flexbox/flex.ts @@ -8,14 +8,11 @@ import { Directive, ElementRef, - Inject, Input, OnChanges, OnDestroy, OnInit, Optional, - PLATFORM_ID, - Renderer2, SimpleChanges, SkipSelf, } from '@angular/core'; @@ -29,6 +26,11 @@ import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from './layout'; import {validateBasis} from '../../utils/basis-validator'; import {isFlowHorizontal} from '../../utils/layout-validator'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** Built-in aliases for different flex-basis values. */ @@ -85,11 +87,14 @@ export class FlexDirective extends BaseFxDirective implements OnInit, OnChanges, // for the parent flex container for this flex item. constructor(monitor: MediaMonitor, elRef: ElementRef, - renderer: Renderer2, @Optional() @SkipSelf() protected _container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { +<<<<<<< Updated upstream + protected styleUtils: StyleUtils) { +======= + protected styleUtils: StylerService) { +>>>>>>> Stashed changes - super(monitor, elRef, renderer, platformId); + super(monitor, elRef, styleUtils); 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..bcf07e04b 100644 --- a/src/lib/api/flexbox/layout-align.ts +++ b/src/lib/api/flexbox/layout-align.ts @@ -13,11 +13,8 @@ import { OnDestroy, OnInit, Optional, - Renderer2, SimpleChanges, Self, - Inject, - PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; import {extendObject} from '../../utils/object-extend'; @@ -28,6 +25,11 @@ import {MediaMonitor} from '../../media-query/media-monitor'; import {LayoutDirective} from './layout'; import {LAYOUT_VALUES, isFlowHorizontal} from '../../utils/layout-validator'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'layout-align' flexbox styling directive @@ -70,10 +72,14 @@ export class LayoutAlignDirective extends BaseFxDirective implements OnInit, OnC /* tslint:enable */ constructor( monitor: MediaMonitor, - elRef: ElementRef, renderer: Renderer2, + elRef: ElementRef, @Optional() @Self() container: LayoutDirective, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); 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..f5bfe515e 100644 --- a/src/lib/api/flexbox/layout-gap.ts +++ b/src/lib/api/flexbox/layout-gap.ts @@ -10,15 +10,12 @@ import { ElementRef, Input, OnChanges, - Renderer2, SimpleChanges, Self, AfterContentInit, Optional, OnDestroy, NgZone, - Inject, - PLATFORM_ID, } from '@angular/core'; import {Subscription} from 'rxjs/Subscription'; @@ -27,6 +24,11 @@ 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'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'layout-padding' styling directive @@ -67,11 +69,14 @@ export class LayoutGapDirective extends BaseFxDirective implements AfterContentI /* tslint:enable */ constructor(monitor: MediaMonitor, elRef: ElementRef, - renderer: Renderer2, @Optional() @Self() container: LayoutDirective, private _zone: NgZone, - @Inject(PLATFORM_ID) platformId: Object) { - super(monitor, elRef, renderer, platformId); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); 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..6a0e6b49c 100644 --- a/src/lib/api/flexbox/layout.ts +++ b/src/lib/api/flexbox/layout.ts @@ -12,10 +12,7 @@ import { OnInit, OnChanges, OnDestroy, - Renderer2, SimpleChanges, - Inject, - PLATFORM_ID, } from '@angular/core'; import {Observable} from 'rxjs/Observable'; @@ -24,6 +21,11 @@ import {MediaChange} from '../../media-query/media-change'; import {MediaMonitor} from '../../media-query/media-monitor'; import {buildLayoutCSS} from '../../utils/layout-validator'; import {ReplaySubject} from 'rxjs/ReplaySubject'; +<<<<<<< Updated upstream +import {StyleUtils} from '../../utils/style-utils'; +======= +import {StylerService} from '../../utils/styling/styler'; +>>>>>>> Stashed changes /** * 'layout' flexbox styling directive * Defines the positioning flow direction for the child elements: row or column @@ -75,9 +77,12 @@ 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); +<<<<<<< Updated upstream + styleUtils: StyleUtils) { +======= + styleUtils: StylerService) { +>>>>>>> Stashed changes + super(monitor, elRef, styleUtils); 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 14378a55c..128675ed7 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 c0a0f87e1..537ef9abb 100644 --- a/src/lib/module.ts +++ b/src/lib/module.ts @@ -31,6 +31,13 @@ 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'; +import {StyleUtils} from './utils/style-utils'; + +import {ServerStylesheet} from './utils/styling/server-stylesheet'; +import {SERVER_PROVIDER} from './utils/styling/server-provider'; +import {StylerService} from './utils/styling/styler'; /** * Since the equivalent results are easily achieved with a css class attached to each @@ -65,7 +72,14 @@ const ALL_DIRECTIVES = [ providers: [ MEDIA_MONITOR_PROVIDER, DEFAULT_BREAKPOINTS_PROVIDER, // Extend defaults with internal custom breakpoints - OBSERVABLE_MEDIA_PROVIDER + OBSERVABLE_MEDIA_PROVIDER, + ServerStylesheet, +<<<<<<< Updated upstream + StyleUtils, +======= + StylerService, +>>>>>>> Stashed changes + SERVER_PROVIDER, ] }) export class FlexLayoutModule { diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 14ca23dec..b5b8920ca 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -12,4 +12,4 @@ export * from './basis-validator'; export * from './layout-validator'; export * from './breakpoint-tools'; export * from './object-extend'; -export * from './style-transforms'; +export * from './styling/index'; diff --git a/src/lib/utils/server-provider.ts b/src/lib/utils/server-provider.ts new file mode 100644 index 000000000..2e209f3c3 --- /dev/null +++ b/src/lib/utils/server-provider.ts @@ -0,0 +1,147 @@ +/** + * @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 renderer = rendererFactory.createRenderer(_document, RENDERER_TYPE); + 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; +} + +const RENDERER_TYPE: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} +}; + +/** + * 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 37049296b..bcc0f216a 100644 --- a/src/lib/utils/style-utils.ts +++ b/src/lib/utils/style-utils.ts @@ -5,95 +5,125 @@ * 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 {Renderer2} from '@angular/core'; +import { + Inject, + Injectable, + PLATFORM_ID, + Renderer2, + RendererFactory2, + RendererType2, + ViewEncapsulation +} from '@angular/core'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; + import {applyCssPrefixes} from './auto-prefixer'; -import {isPlatformBrowser} from '@angular/common'; +import {ServerStylesheet} from '../utils/server-stylesheet'; -/** - * Definition of a css style. Either a property name (e.g. "flex-basis") or an object - * map of property name and value (e.g. {display: 'none', flex-order: 5}). - */ -export type StyleDefinition = string | { [property: string]: string | number | null }; +@Injectable() +export class StyleUtils { + private _renderer: Renderer2; -/** - * Applies styles given via string pair or object map to the directive element. - */ -export function applyStyleToElement(renderer: Renderer2, - element: any, - style: StyleDefinition, - value?: string | number) { - let styles = {}; - if (typeof style === 'string') { - styles[style] = value; - style = styles; + constructor(private _serverStylesheet: ServerStylesheet, + private _rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private _document, + @Inject(PLATFORM_ID) private _platformId) { + this._renderer = this._rendererFactory.createRenderer(this._document, RENDERER_TYPE); } - styles = applyCssPrefixes(style); - applyMultiValueStyleToElement(styles, element, renderer); -} + /** + * Applies styles given via string pair or object map to the directive element. + */ + applyStyleToElement(element: HTMLElement, style: StyleDefinition, value?: string | number) { + if (isPlatformBrowser(this._platformId)) { + let styles = {}; + if (typeof style === 'string') { + styles[style] = value; + style = styles; + } + styles = applyCssPrefixes(style); + this._applyMultiValueStyleToElement(styles, element); + } else { + this._serverStylesheet.addStyleToElement(element, style, value); + } + } + /** + * Applies styles given via string pair or object map to the directive's element. + */ + applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) { + if (isPlatformBrowser(this._platformId)) { + const styles = applyCssPrefixes(style); + elements.forEach(el => { + this._applyMultiValueStyleToElement(styles, el); + }); + } else { + this._serverStylesheet.addStyleToElements(style, elements || []); + } + } -/** - * Applies styles given via string pair or object map to the directive's element. - */ -export function applyStyleToElements(renderer: Renderer2, - style: StyleDefinition, - elements: HTMLElement[ ]) { - let styles = applyCssPrefixes(style); + /** + * Find the DOM element's raw attribute value (if any) + */ + lookupAttributeValue(element: HTMLElement, attribute: string): string { + return element.getAttribute(attribute) || ''; + } - elements.forEach(el => { - applyMultiValueStyleToElement(styles, el, renderer); - }); -} + /** + * Find the DOM element's inline style value (if any) + */ + lookupInlineStyle(element: HTMLElement, styleName: string): string { + return element.style[styleName]; + } -/** - * Applies the styles to the element. The styles object map may contain an array of values. - * Each value will be added as element style. - * Keys are sorted to add prefixed styles (like -webkit-x) first, before the standard ones. - */ -export function applyMultiValueStyleToElement(styles: {}, element: any, renderer: Renderer2) { - Object.keys(styles).sort().forEach(key => { - const values = Array.isArray(styles[key]) ? styles[key] : [styles[key]]; - values.sort(); - for (let value of values) { - renderer.setStyle(element, key, value); + /** + * Determine the inline or inherited CSS style + * @TODO(CaerusKaru): platform-server has no implementation for getComputedStyle + */ + lookupStyle(element: HTMLElement, styleName: string, inlineOnly = false): string { + if (isPlatformBrowser(this._platformId)) { + let value = ''; + if (element) { + let immediateValue = value = this.lookupInlineStyle(element, styleName); + if (!inlineOnly) { + value = immediateValue || getComputedStyle(element).getPropertyValue(styleName); + } + } + + // Note: 'inline' is the default of all elements, unless UA stylesheet overrides; + // in which case getComputedStyle() should determine a valid value. + return value ? value.trim() : 'block'; + } else { + return this._serverStylesheet.getStyleForElement(element, styleName); } - }); -} + } -/** - * Find the DOM element's raw attribute value (if any) - */ -export function lookupAttributeValue(element: HTMLElement, attribute: string): string { - return element.getAttribute(attribute) || ''; -} -/** - * Find the DOM element's inline style value (if any) - */ -export function lookupInlineStyle(element: HTMLElement, styleName: string): string { - return element.style[styleName]; + /** + * Applies the styles to the element. The styles object map may contain an array of values. + * Each value will be added as element style. + * Keys are sorted to add prefixed styles (like -webkit-x) first, before the standard ones. + */ + 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) { + this._renderer.setStyle(element, key, value); + } + }); + } } /** - * Determine the inline or inherited CSS style - * @TODO(CaerusKaru): platform-server has no implementation for getComputedStyle + * Definition of a css style. Either a property name (e.g. "flex-basis") or an object + * map of property name and value (e.g. {display: 'none', flex-order: 5}). */ -export function lookupStyle(_platformId: Object, - element: HTMLElement, - styleName: string, - inlineOnly = false): string { - let value = ''; - if (element) { - let immediateValue = value = lookupInlineStyle(element, styleName); - if (!inlineOnly) { - value = immediateValue || (isPlatformBrowser(_platformId) && - getComputedStyle(element).getPropertyValue(styleName)) || ''; - } - } +export type StyleDefinition = string | { [property: string]: string | number | null }; - // Note: 'inline' is the default of all elements, unless UA stylesheet overrides; - // in which case getComputedStyle() should determine a valid value. - return value ? value.trim() : 'block'; -} +const RENDERER_TYPE: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} +}; diff --git a/src/lib/utils/styling/index.ts b/src/lib/utils/styling/index.ts new file mode 100644 index 000000000..16b26563b --- /dev/null +++ b/src/lib/utils/styling/index.ts @@ -0,0 +1,12 @@ +/** + * @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 + */ + +export * from './styler'; +export * from './style-transforms'; +export * from './server-stylesheet'; +export * from './server-provider'; diff --git a/src/lib/utils/styling/server-provider.ts b/src/lib/utils/styling/server-provider.ts new file mode 100644 index 000000000..a6716ae9a --- /dev/null +++ b/src/lib/utils/styling/server-provider.ts @@ -0,0 +1,147 @@ +/** + * @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 {BREAKPOINTS} from '../../media-query/breakpoints/break-points-token'; +import {BreakPoint} from '../../media-query/breakpoints/break-point'; +import {MatchMedia} from '../../media-query/match-media'; +import {ServerStylesheet} from './server-stylesheet'; + +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 renderer = rendererFactory.createRenderer(_document, RENDERER_TYPE); + 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; +} + +const RENDERER_TYPE: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} +}; + +/** + * 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/styling/server-stylesheet.ts b/src/lib/utils/styling/server-stylesheet.ts new file mode 100644 index 000000000..fe2a80c4e --- /dev/null +++ b/src/lib/utils/styling/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 './styler'; +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-transforms.spec.ts b/src/lib/utils/styling/style-transforms.spec.ts similarity index 96% rename from src/lib/utils/style-transforms.spec.ts rename to src/lib/utils/styling/style-transforms.spec.ts index a48147aed..412143232 100644 --- a/src/lib/utils/style-transforms.spec.ts +++ b/src/lib/utils/styling/style-transforms.spec.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 {customMatchers, expect} from './testing/custom-matchers'; +import {customMatchers, expect} from '../testing/custom-matchers'; import {NgStyleRawList, NgStyleMap, ngStyleUtils as _} from './style-transforms'; describe('ngStyleUtils', () => { diff --git a/src/lib/utils/style-transforms.ts b/src/lib/utils/styling/style-transforms.ts similarity index 100% rename from src/lib/utils/style-transforms.ts rename to src/lib/utils/styling/style-transforms.ts diff --git a/src/lib/utils/style-utils.spec.ts b/src/lib/utils/styling/styler.spec.ts similarity index 94% rename from src/lib/utils/style-utils.spec.ts rename to src/lib/utils/styling/styler.spec.ts index 5e0b8f2cc..3914a4914 100644 --- a/src/lib/utils/style-utils.spec.ts +++ b/src/lib/utils/styling/styler.spec.ts @@ -9,8 +9,8 @@ import {Component} from '@angular/core'; import {CommonModule} from '@angular/common'; import {TestBed} from '@angular/core/testing'; -import {customMatchers} from './testing/custom-matchers'; -import {makeExpectDOMFrom} from './testing/helpers'; +import {customMatchers} from '../testing/custom-matchers'; +import {makeExpectDOMFrom} from '../testing/helpers'; describe('style-utils directive', () => { let expectDOMFrom = makeExpectDOMFrom(() => TestLayoutComponent); diff --git a/src/lib/utils/styling/styler.ts b/src/lib/utils/styling/styler.ts new file mode 100644 index 000000000..bf842ffe3 --- /dev/null +++ b/src/lib/utils/styling/styler.ts @@ -0,0 +1,129 @@ +/** + * @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 { + Inject, + Injectable, + PLATFORM_ID, + Renderer2, + RendererFactory2, + RendererType2, + ViewEncapsulation +} from '@angular/core'; +import {DOCUMENT, isPlatformBrowser} from '@angular/common'; + +import {applyCssPrefixes} from '../auto-prefixer'; +import {ServerStylesheet} from './server-stylesheet'; + +@Injectable() +export class StylerService { + + private _renderer: Renderer2; + + constructor(private _serverStylesheet: ServerStylesheet, + private _rendererFactory: RendererFactory2, + @Inject(DOCUMENT) private _document, + @Inject(PLATFORM_ID) private _platformId) { + this._renderer = this._rendererFactory.createRenderer(this._document, RENDERER_TYPE); + } + + /** + * Applies styles given via string pair or object map to the directive element. + */ + applyStyleToElement(element: HTMLElement, style: StyleDefinition, value?: string | number) { + if (isPlatformBrowser(this._platformId)) { + let styles = {}; + if (typeof style === 'string') { + styles[style] = value; + style = styles; + } + styles = applyCssPrefixes(style); + this._applyMultiValueStyleToElement(styles, element); + } else { + this._serverStylesheet.addStyleToElement(element, style, value); + } + } + + /** + * Applies styles given via string pair or object map to the directive's element. + */ + applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) { + if (isPlatformBrowser(this._platformId)) { + const styles = applyCssPrefixes(style); + elements.forEach(el => { + this._applyMultiValueStyleToElement(styles, el); + }); + } else { + this._serverStylesheet.addStyleToElements(style, elements || []); + } + } + + /** + * Find the DOM element's raw attribute value (if any) + */ + lookupAttributeValue(element: HTMLElement, attribute: string): string { + return element.getAttribute(attribute) || ''; + } + + /** + * Find the DOM element's inline style value (if any) + */ + lookupInlineStyle(element: HTMLElement, styleName: string): string { + return element.style[styleName]; + } + + /** + * Determine the inline or inherited CSS style + * @TODO(CaerusKaru): platform-server has no implementation for getComputedStyle + */ + lookupStyle(element: HTMLElement, styleName: string, inlineOnly = false): string { + if (isPlatformBrowser(this._platformId)) { + let value = ''; + if (element) { + let immediateValue = value = this.lookupInlineStyle(element, styleName); + if (!inlineOnly) { + value = immediateValue || getComputedStyle(element).getPropertyValue(styleName); + } + } + + // Note: 'inline' is the default of all elements, unless UA stylesheet overrides; + // in which case getComputedStyle() should determine a valid value. + return value ? value.trim() : 'block'; + } else { + return this._serverStylesheet.getStyleForElement(element, styleName); + } + } + + /** + * Applies the styles to the element. The styles object map may contain an array of values. + * Each value will be added as element style. + * Keys are sorted to add prefixed styles (like -webkit-x) first, before the standard ones. + */ + 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) { + this._renderer.setStyle(element, key, value); + } + }); + } +} + +/** + * Definition of a css style. Either a property name (e.g. "flex-basis") or an object + * map of property name and value (e.g. {display: 'none', flex-order: 5}). + */ +export type StyleDefinition = string | { [property: string]: string | number | null }; + +const RENDERER_TYPE: RendererType2 = { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} +}; + 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