Skip to content

Commit

Permalink
feat(ssr): enhance support for Universal and SSR with stylesheets
Browse files Browse the repository at this point in the history
* Add `StyleService` 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
* While not in the browser (ssr), intercept all style calls and reroute them to the virtual
  stylesheet.
* For server-side rendering, add a new type of MediaQueryList similar to the MockMediaQueryList
  to support manual activation/deactivation of breakpoints
* Add jasmine testing mode for SSR
* Add FlexLayoutServerModule to invoke SSR styling
* Remove unnecessary Renderer references and replace them with DOM APIs
* Add whitespace debugging mode for server styles

Fixes #373.

> See [Design Doc](https://docs.google.com/document/d/1fg04ihw42dJJHGd6fugdiBe39iJot8aErhiE7CjwfmQ/edit#)
  • Loading branch information
CaerusKaru committed Feb 13, 2018
1 parent 7699957 commit 4d9e74f
Show file tree
Hide file tree
Showing 62 changed files with 1,832 additions and 1,095 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- env: "MODE=lint"
- env: "MODE=aot"
- env: "MODE=prerender"
- env: "MODE=ssr"
# Closure Compiler CI check is temporarily disabled until a new version of
# the tool is released with https://github.com/google/closure-compiler/pull/2600
# - env: "MODE=closure-compiler"
Expand All @@ -38,6 +39,10 @@ env:
- BROWSER_PROVIDER_READY_FILE=/tmp/flex-layout-build/readyfile
- BROWSER_PROVIDER_ERROR_FILE=/tmp/flex-layout-build/errorfile

matrix:
allow_failures:
- env: "MODE=ssr"


before_install:
- source ./scripts/ci/env.sh
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"lib:build:aot": "gulp ci:aot",
"lib:lint": "gulp lint",
"lib:test": "gulp test",
"lib:test:ssr": "gulp test:ssr",
"universal:build": "gulp universal:build",
"universal:ci:prerender": "gulp ci:prerender"
},
Expand Down
4 changes: 4 additions & 0 deletions scripts/ci/sources/mode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,7 @@ is_unit() {
is_prerender() {
[[ "$MODE" = prerender ]]
}

is_ssr() {
[[ "$MODE" = ssr ]]
}
2 changes: 2 additions & 0 deletions scripts/ci/travis-testing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ elif is_prerender; then
$(npm bin)/gulp ci:prerender
elif is_closure_compiler; then
./scripts/closure-compiler/build-devapp-bundle.sh
elif is_ssr; then
$(npm bin)/gulp ci:ssr
fi

teardown_tunnel
15 changes: 4 additions & 11 deletions src/lib/api/core/base-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +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 {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';

export class MockElementRef extends ElementRef {
constructor() {
const nEl = document.createElement('DIV');
super(nEl);
this.nativeElement = nEl;
}
}
import {MediaMonitor} from '../../media-query/media-monitor';
import {StyleService} from '../../utils/styling/styler';

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, {} as ElementRef, {} as StyleService); // tslint:disable-line:max-line-length
});
describe('cacheInput', () => {
it('should call _cacheInputArray when source is an array', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/api/core/base-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
* 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';
import {StyleService} from '../../utils/styling/styler';


/**
Expand Down Expand Up @@ -48,9 +49,8 @@ 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);
protected _styler: StyleService) {
super(_mediaMonitor, _elementRef, _styler);
}

/**
Expand Down
44 changes: 19 additions & 25 deletions src/lib/api/core/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,13 @@ import {
SimpleChanges,
OnChanges,
SimpleChange,
Renderer2,
Inject,
PLATFORM_ID,
} from '@angular/core';

import {buildLayoutCSS} from '../../utils/layout-validator';
import {
StyleDefinition,
lookupStyle,
lookupInlineStyle,
applyStyleToElement,
applyStyleToElements,
lookupAttributeValue,
} from '../../utils/style-utils';
StyleService,
} from '../../utils/styling/styler';

import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation';
import {MediaMonitor} from '../../media-query/media-monitor';
Expand Down Expand Up @@ -70,8 +63,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
*/
constructor(protected _mediaMonitor: MediaMonitor,
protected _elementRef: ElementRef,
protected _renderer: Renderer2,
@Inject(PLATFORM_ID) protected _platformId: Object) {
protected _styler: StyleService) {
}

// *********************************************
Expand All @@ -85,7 +77,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
return this._elementRef.nativeElement.parentNode;
}

protected get nativeElement(): any {
protected get nativeElement(): HTMLElement {
return this._elementRef.nativeElement;
}

Expand Down Expand Up @@ -137,19 +129,20 @@ 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';
return this._styler.lookupStyle(source, query);
}

/**
* Quick accessor to raw attribute value on the target DOM element
*/
protected _getAttributeValue(attribute: string,
source: HTMLElement = this.nativeElement): string {
return lookupAttributeValue(source || this.nativeElement, attribute);
return this._styler.lookupAttributeValue(source, attribute);
}

/**
Expand All @@ -158,36 +151,37 @@ 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 = '';

if (target) {
value = lookupStyle(this._platformId, target, 'flex-direction') || 'row';
let hasInlineValue = lookupInlineStyle(target, 'flex-direction');
[value, hasInlineValue] = this._styler.getFlowDirection(target);

if (!hasInlineValue && addIfMissing) {
applyStyleToElements(this._renderer, buildLayoutCSS(value), [target]);
const style = buildLayoutCSS(value);
const elements = [target];
this._styler.applyStyleToElements(style, elements);
}
}

return value.trim();
return value.trim() || 'row';
}

/**
* Applies styles given via string pair or object map to the directive element.
*/
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) {
this._styler.applyStyleToElement(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 || []);
protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) {
this._styler.applyStyleToElements(style, elements);
}

/**
Expand Down
24 changes: 18 additions & 6 deletions src/lib/api/ext/class.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {Component} from '@angular/core';
import {CommonModule} from '@angular/common';
import {Component, PLATFORM_ID} from '@angular/core';
import {CommonModule, isPlatformServer} from '@angular/common';
import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';

import {customMatchers, expect} from '../../utils/testing/custom-matchers';
Expand All @@ -21,15 +21,19 @@ import {BreakPointRegistry} from '../../media-query/breakpoints/break-point-regi

import {ClassDirective} from './class';
import {MediaQueriesModule} from '../../media-query/_module';
import {ServerStylesheet} from '../../utils/styling/server-stylesheet';
import {StyleService} from '../../utils/styling/styler';

describe('class directive', () => {
let fixture: ComponentFixture<any>;
let matchMedia: MockMatchMedia;
let platformId: Object;
let createTestComponent = (template: string) => {
fixture = makeCreateTestComponent(() => TestClassComponent)(template);

inject([MatchMedia], (_matchMedia: MockMatchMedia) => {
inject([MatchMedia, PLATFORM_ID], (_matchMedia: MockMatchMedia, _platformId: Object) => {
matchMedia = _matchMedia;
platformId = _platformId;
})();
};

Expand All @@ -46,7 +50,9 @@ describe('class directive', () => {
declarations: [TestClassComponent, ClassDirective],
providers: [
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
{provide: MatchMedia, useClass: MockMatchMedia}
{provide: MatchMedia, useClass: MockMatchMedia},
ServerStylesheet,
StyleService,
]
});
});
Expand Down Expand Up @@ -224,15 +230,21 @@ describe('class directive', () => {
fixture.detectChanges();
let button = queryFor(fixture, '[mat-raised-button]')[0].nativeElement;

expect(button).toHaveCssClass('mat-raised-button');
// TODO(CaerusKaru): MatButton doesn't apply host attributes on the server
if (!isPlatformServer(platformId)) {
expect(button).toHaveCssClass('mat-raised-button');
}
expect(button).toHaveCssClass('btn-xs');
expect(button).toHaveCssClass('mat-primary');

fixture.componentInstance.formButtonXs = false;
fixture.detectChanges();
button = queryFor(fixture, '[mat-raised-button]')[0].nativeElement;

expect(button).toHaveCssClass('mat-raised-button');
// TODO(CaerusKaru): MatButton doesn't apply host attributes on the server
if (!isPlatformServer(platformId)) {
expect(button).toHaveCssClass('mat-raised-button');
}
expect(button).not.toHaveCssClass('btn-xs');
expect(button).toHaveCssClass('mat-primary');
});
Expand Down
14 changes: 8 additions & 6 deletions src/lib/api/ext/class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {
SimpleChanges,
Self,
OnInit,
Inject,
PLATFORM_ID,
} from '@angular/core';
import {NgClass} from '@angular/common';

Expand All @@ -29,14 +27,15 @@ 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 {StyleService} from '../../utils/styling/styler';

/** NgClass allowed inputs **/
export type NgClassType = string | string[] | Set<string> | {[klass: string]: any};

/**
* Directive to add responsive support for ngClass.
* This maintains the core functionality of 'ngClass' and adds responsive API
*
* Note: this class is a no-op when rendered on the server
*/
@Directive({
selector: `
Expand Down Expand Up @@ -95,8 +94,8 @@ 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);
protected _styler: StyleService) {
super(monitor, _ngEl, _styler);
this._configureAdapters();
}

Expand Down Expand Up @@ -139,7 +138,10 @@ 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._styler
);
if (!this._ngClassInstance) {
// Create an instance NgClass Directive instance only if `ngClass=""` has NOT been defined on
Expand Down
Loading

0 comments on commit 4d9e74f

Please sign in to comment.