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

Commit cf5266a

Browse files
CaerusKaruThomasBurleson
authored andcommitted
feat(ssr): enhance support for Universal and SSR with stylesheets
* 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. Closes #567. > See [Design Doc](https://docs.google.com/document/d/1fg04ihw42dJJHGd6fugdiBe39iJot8aErhiE7CjwfmQ/edit#)
1 parent 04b9bfd commit cf5266a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2021
-1138
lines changed

.travis.yml

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jobs:
2121
- env: "MODE=lint"
2222
- env: "MODE=aot"
2323
- env: "MODE=prerender"
24+
- env: "MODE=ssr"
2425
- env: "MODE=saucelabs_required"
2526
- env: "MODE=browserstack_required"
2627
- env: "MODE=travis_required"
@@ -39,6 +40,10 @@ env:
3940
- BROWSER_PROVIDER_READY_FILE=/tmp/flex-layout-build/readyfile
4041
- BROWSER_PROVIDER_ERROR_FILE=/tmp/flex-layout-build/errorfile
4142

43+
matrix:
44+
allow_failures:
45+
- env: "MODE=ssr"
46+
4247

4348
before_install:
4449
- source ./scripts/ci/env.sh

build-config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* Build configuration for the packaging tool. This file will be automatically detected and used
3-
* to build the different packages inside of Material.
3+
* to build the different packages inside of Layout.
44
*/
55
const {join} = require('path');
66

guides/SSR.md

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Using Flex Layout with Server-Side Rendering (SSR)
2+
3+
### Introduction
4+
5+
In the browser, Flex Layout works by utilizing the global `Window` object's
6+
`MatchMedia` interface. When a breakpoint is activated/deactivated, the service
7+
informs the Flex directives, which inject CSS styles inline as necessary.
8+
9+
Unfortunately, on the server, we have no access to the `MatchMedia` service,
10+
and so when the view is rendered for users using SSR, none of the responsive
11+
breakpoints (i.e. `fxFlex.sm`) are respected. This leads to a mismatch between
12+
the initial view generated by the server, and the bootstrapped view generated
13+
by the client.
14+
15+
The solution provided allows Flex Layout to inject static CSS into the head of
16+
the DOM instead of inline, and taps in to the CSS `@media` breakpoint interface,
17+
instead of the dynamic JavaScript `MatchMedia` interface.
18+
19+
This guide introduces how to incorporate this functionality into your apps, and
20+
the limitations to be aware of when using this utility.
21+
22+
### Usage
23+
24+
#### Option 1: Generate static CSS on the server
25+
26+
1. Import the `FlexLayoutServerModule` into the server bundle for your app,
27+
generally called `app.server.module.ts`:
28+
29+
```typescript
30+
import {NgModule} from '@angular/core';
31+
import {FlexLayoutServerModule} from '@angular/flex-layout/server';
32+
33+
@NgModule(({
34+
imports: [
35+
... other imports here
36+
FlexLayoutServerModule,
37+
]
38+
}))
39+
export class AppServerModule {}
40+
```
41+
42+
2. That's it! Your app should now be configured to use the server-side
43+
implementations of the Flex Layout utilities.
44+
45+
46+
#### Option 2: Only generate inline styles (legacy option)
47+
48+
1. Simply don't import the `FlexLayoutServerModule`. You'll receive a warning
49+
on bootstrap, but this won't prevent you from using the library, and the
50+
warning won't be logged on the client side
51+
52+
53+
#### Option 3: Generate no Flex Layout stylings on the server
54+
55+
1. Don't import the `FlexLayoutServerModule`
56+
2. DO import the `SERVER_TOKEN` and provide it in your app as follows:
57+
58+
```typescript
59+
import {SERVER_TOKEN} from '@angular/flex-layout';
60+
61+
{provide: SERVER_TOKEN, useValue: true}
62+
```
63+
64+
3. This will tell Flex Layout to not generate server stylings. Please note that
65+
if you provide this token *and* the `FlexLayoutServerModule`, stylings **will**
66+
still be rendered
67+
68+
### Limitations
69+
70+
One of the deficiencies of SSR is the lack of a fully-capable DOM rendering
71+
engine. As such, some functionality of the Flex Layout library is imparied.
72+
For instance, some Flex directives search for parent nodes with flex stylings
73+
applied to avoid overwriting styles. However, if those styles are defined in
74+
a style block, the external component styles, or another stylesheet, Flex Layout
75+
won't be able to find those styles on the server.
76+
77+
The workaround for this is to **inline all Flex-related styles** as necessary.
78+
For instance, if in an external stylesheet you have a class that applies
79+
`flex-direction` to an element, add that styling inline on the element the
80+
class is applied to. Chances are the impact of this will be minimal, and the
81+
stylings will be loaded correctly on bootstrap. However, it is an unfortunate
82+
reality of SSR and the DOM implementation used on the server.
83+
84+
### References
85+
86+
The design doc for this utility can be found
87+
[here](https://docs.google.com/document/d/1fg04ihw42dJJHGd6fugdiBe39iJot8aErhiE7CjwfmQ)

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"lib:build:aot": "gulp ci:aot",
1818
"lib:lint": "gulp lint",
1919
"lib:test": "gulp test",
20+
"lib:test:ssr": "gulp test:ssr",
2021
"universal:build": "gulp universal:build",
2122
"universal:ci:prerender": "gulp ci:prerender"
2223
},

scripts/ci/sources/mode.sh

+4
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ is_unit() {
2828
is_prerender() {
2929
[[ "$MODE" = prerender ]]
3030
}
31+
32+
is_ssr() {
33+
[[ "$MODE" = ssr ]]
34+
}

scripts/ci/travis-testing.sh

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ elif is_prerender; then
4141
$(npm bin)/gulp ci:prerender
4242
elif is_closure_compiler; then
4343
./scripts/closure-compiler/build-devapp-bundle.sh
44+
elif is_ssr; then
45+
$(npm bin)/gulp ci:ssr
4446
fi
4547

4648
teardown_tunnel

src/lib/api/core/base-adapter.spec.ts

+4-11
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,16 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {ElementRef, Renderer2} from '@angular/core';
8+
import {ElementRef} from '@angular/core';
99
import {BaseFxDirectiveAdapter} from './base-adapter';
1010
import {expect} from '../../utils/testing/custom-matchers';
11-
import {MediaMonitor} from '@angular/flex-layout/media-query';
12-
13-
export class MockElementRef extends ElementRef {
14-
constructor() {
15-
const nEl = document.createElement('DIV');
16-
super(nEl);
17-
this.nativeElement = nEl;
18-
}
19-
}
11+
import {MediaMonitor} from '../../media-query/media-monitor';
12+
import {StyleUtils} from '../../utils/styling/style-utils';
2013

2114
describe('BaseFxDirectiveAdapter class', () => {
2215
let component;
2316
beforeEach(() => {
24-
component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, new MockElementRef(), {} as Renderer2, {}); // tslint:disable-line:max-line-length
17+
component = new BaseFxDirectiveAdapter('', {} as MediaMonitor, {} as ElementRef, {} as StyleUtils); // tslint:disable-line:max-line-length
2518
});
2619
describe('cacheInput', () => {
2720
it('should call _cacheInputArray when source is an array', () => {

src/lib/api/core/base-adapter.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {ElementRef, Inject, PLATFORM_ID, Renderer2} from '@angular/core';
8+
import {ElementRef} from '@angular/core';
99

1010
import {BaseFxDirective} from './base';
1111
import {ResponsiveActivation} from './responsive-activation';
1212
import {MediaQuerySubscriber} from '../../media-query/media-change';
1313
import {MediaMonitor} from '../../media-query/media-monitor';
14+
import {StyleUtils} from '../../utils/styling/style-utils';
1415

1516

1617
/**
@@ -48,9 +49,8 @@ export class BaseFxDirectiveAdapter extends BaseFxDirective {
4849
constructor(protected _baseKey: string, // non-responsive @Input property name
4950
protected _mediaMonitor: MediaMonitor,
5051
protected _elementRef: ElementRef,
51-
protected _renderer: Renderer2,
52-
@Inject(PLATFORM_ID) protected _platformId: Object) {
53-
super(_mediaMonitor, _elementRef, _renderer, _platformId);
52+
protected _styler: StyleUtils) {
53+
super(_mediaMonitor, _elementRef, _styler);
5454
}
5555

5656
/**

src/lib/api/core/base.ts

+19-25
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,13 @@ import {
1111
SimpleChanges,
1212
OnChanges,
1313
SimpleChange,
14-
Renderer2,
15-
Inject,
16-
PLATFORM_ID,
1714
} from '@angular/core';
1815

1916
import {buildLayoutCSS} from '../../utils/layout-validator';
2017
import {
2118
StyleDefinition,
22-
lookupStyle,
23-
lookupInlineStyle,
24-
applyStyleToElement,
25-
applyStyleToElements,
26-
lookupAttributeValue,
27-
} from '../../utils/style-utils';
19+
StyleUtils,
20+
} from '../../utils/styling/style-utils';
2821

2922
import {ResponsiveActivation, KeyOptions} from '../core/responsive-activation';
3023
import {MediaMonitor} from '../../media-query/media-monitor';
@@ -70,8 +63,7 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
7063
*/
7164
constructor(protected _mediaMonitor: MediaMonitor,
7265
protected _elementRef: ElementRef,
73-
protected _renderer: Renderer2,
74-
@Inject(PLATFORM_ID) protected _platformId: Object) {
66+
protected _styler: StyleUtils) {
7567
}
7668

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

88-
protected get nativeElement(): any {
80+
protected get nativeElement(): HTMLElement {
8981
return this._elementRef.nativeElement;
9082
}
9183

@@ -137,19 +129,20 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
137129

138130
/**
139131
* Quick accessor to the current HTMLElement's `display` style
140-
* Note: this allows use to preserve the original style
132+
* Note: this allows us to preserve the original style
141133
* and optional restore it when the mediaQueries deactivate
142134
*/
143135
protected _getDisplayStyle(source: HTMLElement = this.nativeElement): string {
144-
return lookupStyle(this._platformId, source || this.nativeElement, 'display');
136+
const query = 'display';
137+
return this._styler.lookupStyle(source, query);
145138
}
146139

147140
/**
148141
* Quick accessor to raw attribute value on the target DOM element
149142
*/
150143
protected _getAttributeValue(attribute: string,
151144
source: HTMLElement = this.nativeElement): string {
152-
return lookupAttributeValue(source || this.nativeElement, attribute);
145+
return this._styler.lookupAttributeValue(source, attribute);
153146
}
154147

155148
/**
@@ -158,36 +151,37 @@ export abstract class BaseFxDirective implements OnDestroy, OnChanges {
158151
* Check inline style first then check computed (stylesheet) style.
159152
* And optionally add the flow value to element's inline style.
160153
*/
161-
protected _getFlowDirection(target: any, addIfMissing = false): string {
154+
protected _getFlowDirection(target: HTMLElement, addIfMissing = false): string {
162155
let value = 'row';
156+
let hasInlineValue = '';
163157

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

168161
if (!hasInlineValue && addIfMissing) {
169-
applyStyleToElements(this._renderer, buildLayoutCSS(value), [target]);
162+
const style = buildLayoutCSS(value);
163+
const elements = [target];
164+
this._styler.applyStyleToElements(style, elements);
170165
}
171166
}
172167

173-
return value.trim();
168+
return value.trim() || 'row';
174169
}
175170

176171
/**
177172
* Applies styles given via string pair or object map to the directive element.
178173
*/
179174
protected _applyStyleToElement(style: StyleDefinition,
180175
value?: string | number,
181-
nativeElement: any = this.nativeElement) {
182-
let element = nativeElement || this.nativeElement;
183-
applyStyleToElement(this._renderer, element, style, value);
176+
element: HTMLElement = this.nativeElement) {
177+
this._styler.applyStyleToElement(element, style, value);
184178
}
185179

186180
/**
187181
* Applies styles given via string pair or object map to the directive's element.
188182
*/
189-
protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[ ]) {
190-
applyStyleToElements(this._renderer, style, elements || []);
183+
protected _applyStyleToElements(style: StyleDefinition, elements: HTMLElement[]) {
184+
this._styler.applyStyleToElements(style, elements);
191185
}
192186

193187
/**

src/lib/api/ext/class.spec.ts

+18-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {Component} from '@angular/core';
9-
import {CommonModule} from '@angular/common';
8+
import {Component, PLATFORM_ID} from '@angular/core';
9+
import {CommonModule, isPlatformServer} from '@angular/common';
1010
import {ComponentFixture, TestBed, async, inject} from '@angular/core/testing';
1111

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

2222
import {ClassDirective} from './class';
2323
import {MediaQueriesModule} from '../../media-query/_module';
24+
import {ServerStylesheet} from '../../utils/styling/server-stylesheet';
25+
import {StyleUtils} from '../../utils/styling/style-utils';
2426

2527
describe('class directive', () => {
2628
let fixture: ComponentFixture<any>;
2729
let matchMedia: MockMatchMedia;
30+
let platformId: Object;
2831
let createTestComponent = (template: string) => {
2932
fixture = makeCreateTestComponent(() => TestClassComponent)(template);
3033

31-
inject([MatchMedia], (_matchMedia: MockMatchMedia) => {
34+
inject([MatchMedia, PLATFORM_ID], (_matchMedia: MockMatchMedia, _platformId: Object) => {
3235
matchMedia = _matchMedia;
36+
platformId = _platformId;
3337
})();
3438
};
3539

@@ -46,7 +50,9 @@ describe('class directive', () => {
4650
declarations: [TestClassComponent, ClassDirective],
4751
providers: [
4852
BreakPointRegistry, DEFAULT_BREAKPOINTS_PROVIDER,
49-
{provide: MatchMedia, useClass: MockMatchMedia}
53+
{provide: MatchMedia, useClass: MockMatchMedia},
54+
ServerStylesheet,
55+
StyleUtils,
5056
]
5157
});
5258
});
@@ -224,15 +230,21 @@ describe('class directive', () => {
224230
fixture.detectChanges();
225231
let button = queryFor(fixture, '[mat-raised-button]')[0].nativeElement;
226232

227-
expect(button).toHaveCssClass('mat-raised-button');
233+
// TODO(CaerusKaru): MatButton doesn't apply host attributes on the server
234+
if (!isPlatformServer(platformId)) {
235+
expect(button).toHaveCssClass('mat-raised-button');
236+
}
228237
expect(button).toHaveCssClass('btn-xs');
229238
expect(button).toHaveCssClass('mat-primary');
230239

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

235-
expect(button).toHaveCssClass('mat-raised-button');
244+
// TODO(CaerusKaru): MatButton doesn't apply host attributes on the server
245+
if (!isPlatformServer(platformId)) {
246+
expect(button).toHaveCssClass('mat-raised-button');
247+
}
236248
expect(button).not.toHaveCssClass('btn-xs');
237249
expect(button).toHaveCssClass('mat-primary');
238250
});

0 commit comments

Comments
 (0)