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

Commit 9df0fa2

Browse files
committed
fix(server): disable breakpoints correctly and avoid style overuse
This problem is two-fold. First, on the server, when we deactivated breakpoints, we were deactivating them in the wrong order. This meant that after a breakpoint was activated, its styles would linger, polluting the correct state of the style map. The second problem was overpopulation of styles. Essentially, we would provide a fallback style for every single breakpoint if available. This is redundant, because we already populate a global activation state at the beginning of style population. To resolve these issues, we ensure that we deactivate breakpoints correctly, and we also disable fallback styles on the server after the initial population is complete.
1 parent 51b6ebf commit 9df0fa2

File tree

11 files changed

+923
-84
lines changed

11 files changed

+923
-84
lines changed

angular.json

+12-1
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
"builder": "@angular-devkit/build-angular:server",
186186
"options": {
187187
"outputPath": "dist/universal-demo-app/server",
188-
"main": "projects/apps/universal-demo-app/src/main.server.ts",
188+
"main": "projects/apps/universal-demo-app/server.ts",
189189
"tsConfig": "projects/apps/universal-demo-app/tsconfig.server.json",
190190
"preserveSymlinks": true,
191191
"sourceMap": true,
@@ -203,6 +203,17 @@
203203
}
204204
}
205205
},
206+
"serve-ssr": {
207+
"builder": "@nguniversal/builders:ssr-dev-server",
208+
"defaultConfiguration": "production",
209+
"options": {},
210+
"configurations": {
211+
"production": {
212+
"browserTarget": "universal-demo-app:build:production",
213+
"serverTarget": "universal-demo-app:server:production"
214+
}
215+
}
216+
},
206217
"serve": {
207218
"builder": "@angular-devkit/build-angular:dev-server",
208219
"configurations": {

package.json

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@
2121
"demo:serve": "ng serve demo-app",
2222
"stamp": "ts-node tools/package-tools/version-placeholders.ts --module=commonjs",
2323
"build:universal-demo-app": "ng run universal-demo-app:build:production && ng run universal-demo-app:server",
24+
"serve:universal-demo-app": "ng run universal-demo-app:serve-ssr",
2425
"test": "ng test @angular/flex-layout",
2526
"test:ssr": "webpack --config test/webpack-spec-ssr-bundle.js && jasmine --config=test/jasmine-ssr.json",
2627
"lint": "stylelint projects/libs/**/*.scss --config=stylelint-config.json && tslint --project ./tsconfig.json --config tslint.json",
2728
"release": "yarn build && yarn stamp",
28-
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s"
29+
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
30+
"dev:ssr": "ng run universal-demo-app:serve-ssr",
31+
"serve:ssr": "node dist/universal-demo-app/server/main.js",
32+
"build:ssr": "ng build && ng run universal-demo-app:server",
33+
"prerender": "ng run universal-demo-app:prerender"
2934
},
3035
"private": true,
3136
"requiredAngularVersion": "^13.0.0",
@@ -40,8 +45,8 @@
4045
"@angular/platform-browser-dynamic": "~13.0.0",
4146
"@angular/platform-server": "~13.0.0",
4247
"@angular/router": "~13.0.0",
48+
"@nguniversal/builders": "^13.0.1",
4349
"@nguniversal/express-engine": "^13.0.1",
44-
"@types/express": "^4.17.13",
4550
"express": "^4.17.1",
4651
"rxjs": "~7.4.0",
4752
"tslib": "^2.3.0",
@@ -53,6 +58,8 @@
5358
"@angular/cli": "~13.0.0",
5459
"@angular/compiler-cli": "~13.0.0",
5560
"@ngtools/webpack": "^13.0.3",
61+
"@nguniversal/builders": "^13.0.1",
62+
"@types/express": "^4.17.0",
5663
"@types/jasmine": "~3.10.0",
5764
"@types/minimatch": "^3.0.5",
5865
"@types/node": "^12.11.1",
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import 'zone.js/dist/zone-node';
2+
3+
import { ngExpressEngine } from '@nguniversal/express-engine';
4+
import * as express from 'express';
5+
import { join } from 'path';
6+
7+
import { AppServerModule } from './src/app/app.server.module';
8+
import { APP_BASE_HREF } from '@angular/common';
9+
import { existsSync } from 'fs';
10+
11+
// The Express app is exported so that it can be used by serverless Functions.
12+
export function app(): express.Express {
13+
const server = express();
14+
const distFolder = join(process.cwd(), 'dist/universal-demo-app/browser');
15+
const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
16+
17+
// Our Universal express-engine (found at
18+
// https://github.com/angular/universal/tree/master/modules/express-engine)
19+
server.engine('html', ngExpressEngine({
20+
bootstrap: AppServerModule,
21+
}));
22+
23+
server.set('view engine', 'html');
24+
server.set('views', distFolder);
25+
26+
// Example Express Rest API endpoints
27+
// server.get('/api/**', (req, res) => { });
28+
// Serve static files from /browser
29+
server.get('*.*', express.static(distFolder, {
30+
maxAge: '1y'
31+
}));
32+
33+
// All regular routes use the Universal engine
34+
server.get('*', (req, res) => {
35+
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
36+
});
37+
38+
return server;
39+
}
40+
41+
function run(): void {
42+
const port = process.env['PORT'] || 4000;
43+
44+
// Start up the Node server
45+
const server = app();
46+
server.listen(port, () => {
47+
console.log(`Node Express server listening on http://localhost:${port}`);
48+
});
49+
}
50+
51+
// Webpack will replace 'require' with '__webpack_require__'
52+
// '__non_webpack_require__' is a proxy to Node 'require'
53+
// The below code is to ensure that the server is run only when not requiring the bundle.
54+
declare const __non_webpack_require__: NodeRequire;
55+
const mainModule = __non_webpack_require__.main;
56+
const moduleFilename = mainModule && mainModule.filename || '';
57+
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
58+
run();
59+
}
60+
61+
export * from './src/main.server';

projects/apps/universal-demo-app/tsconfig.server.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"entryModule": "app/app.server.module#AppServerModule"
1212
},
1313
"files": [
14-
"src/main.server.ts"
14+
"src/main.server.ts",
15+
"server.ts"
1516
]
1617
}

projects/libs/flex-layout/core/breakpoints/break-point-registry.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,19 @@ export class BreakPointRegistry {
3030
* Search breakpoints by alias (e.g. gt-xs)
3131
*/
3232
findByAlias(alias: string): OptionalBreakPoint {
33-
return !alias ? null : this.findWithPredicate(alias, (bp) => bp.alias == alias);
33+
return !alias ? null : this.findWithPredicate(alias, (bp) => bp.alias === alias);
3434
}
3535

3636
findByQuery(query: string): OptionalBreakPoint {
37-
return this.findWithPredicate(query, (bp) => bp.mediaQuery == query);
37+
return this.findWithPredicate(query, (bp) => bp.mediaQuery === query);
3838
}
3939

4040
/**
4141
* Get all the breakpoints whose ranges could overlapping `normal` ranges;
4242
* e.g. gt-sm overlaps md, lg, and xl
4343
*/
4444
get overlappings(): BreakPoint[] {
45-
return this.items.filter(it => it.overlapping == true);
45+
return this.items.filter(it => it.overlapping);
4646
}
4747

4848
/**
@@ -58,7 +58,7 @@ export class BreakPointRegistry {
5858
* for property layoutGtSM.
5959
*/
6060
get suffixes(): string[] {
61-
return this.items.map(it => !!it.suffix ? it.suffix : '');
61+
return this.items.map(it => it?.suffix ?? '');
6262
}
6363

6464
/**
@@ -68,10 +68,10 @@ export class BreakPointRegistry {
6868
searchFn: (bp: BreakPoint) => boolean): OptionalBreakPoint {
6969
let response = this.findByMap.get(key);
7070
if (!response) {
71-
response = this.items.find(searchFn) || null;
71+
response = this.items.find(searchFn) ?? null;
7272
this.findByMap.set(key, response);
7373
}
74-
return response || null;
74+
return response ?? null;
7575

7676
}
7777

projects/libs/flex-layout/core/match-media/match-media.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export class MatchMedia implements OnDestroy {
4949
*/
5050
isActive(mediaQuery: string): boolean {
5151
const mql = this.registry.get(mediaQuery);
52-
return !!mql ? mql.matches : this.registerQuery(mediaQuery).some(m => m.matches);
52+
return mql?.matches ?? this.registerQuery(mediaQuery).some(m => m.matches);
5353
}
5454

5555
/**

projects/libs/flex-layout/core/media-marshaller/media-marshaller.ts

+24-12
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface ElementMatcher {
4343
*/
4444
@Injectable({providedIn: 'root'})
4545
export class MediaMarshaller {
46+
private _useFallbacks = true;
4647
private activatedBreakpoints: BreakPoint[] = [];
4748
private elementMap: ElementMap = new Map();
4849
private elementKeyMap: ElementKeyMap = new WeakMap();
@@ -53,7 +54,11 @@ export class MediaMarshaller {
5354
private subject: Subject<ElementMatcher> = new Subject();
5455

5556
get activatedAlias(): string {
56-
return this.activatedBreakpoints[0] ? this.activatedBreakpoints[0].alias : '';
57+
return this.activatedBreakpoints[0]?.alias ?? '';
58+
}
59+
60+
set useFallbacks(value: boolean) {
61+
this._useFallbacks = value;
5762
}
5863

5964
constructor(protected matchMedia: MatchMedia,
@@ -68,18 +73,20 @@ export class MediaMarshaller {
6873
*/
6974
onMediaChange(mc: MediaChange) {
7075
const bp: BreakPoint | null = this.findByQuery(mc.mediaQuery);
76+
7177
if (bp) {
7278
mc = mergeAlias(mc, bp);
7379

74-
if (mc.matches && this.activatedBreakpoints.indexOf(bp) === -1) {
80+
const bpIndex = this.activatedBreakpoints.indexOf(bp);
81+
82+
if (mc.matches && bpIndex === -1) {
7583
this.activatedBreakpoints.push(bp);
7684
this.activatedBreakpoints.sort(sortDescendingPriority);
7785

7886
this.updateStyles();
79-
80-
} else if (!mc.matches && this.activatedBreakpoints.indexOf(bp) !== -1) {
87+
} else if (!mc.matches && bpIndex !== -1) {
8188
// Remove the breakpoint when it's deactivated
82-
this.activatedBreakpoints.splice(this.activatedBreakpoints.indexOf(bp), 1);
89+
this.activatedBreakpoints.splice(bpIndex, 1);
8390
this.activatedBreakpoints.sort(sortDescendingPriority);
8491

8592
this.updateStyles();
@@ -193,7 +200,6 @@ export class MediaMarshaller {
193200
this.clearElement(el, k);
194201
}
195202
});
196-
197203
});
198204
}
199205

@@ -204,6 +210,7 @@ export class MediaMarshaller {
204210
*/
205211
clearElement(element: HTMLElement, key: string): void {
206212
const builders = this.clearMap.get(element);
213+
207214
if (builders) {
208215
const clearFn: ClearCallback = builders.get(key) as ClearCallback;
209216
if (!!clearFn) {
@@ -316,12 +323,20 @@ export class MediaMarshaller {
316323
for (let i = 0; i < this.activatedBreakpoints.length; i++) {
317324
const activatedBp = this.activatedBreakpoints[i];
318325
const valueMap = bpMap.get(activatedBp.alias);
326+
319327
if (valueMap) {
320328
if (key === undefined || (valueMap.has(key) && valueMap.get(key) != null)) {
321329
return valueMap;
322330
}
323331
}
324332
}
333+
334+
// On the server, we explicitly have an "all" section filled in to begin with.
335+
// So we don't need to aggressively find a fallback if no explicit value exists.
336+
if (!this._useFallbacks) {
337+
return undefined;
338+
}
339+
325340
const lastHope = bpMap.get('');
326341
return (key === undefined || lastHope && lastHope.has(key)) ? lastHope : undefined;
327342
}
@@ -347,14 +362,11 @@ export class MediaMarshaller {
347362
function initBuilderMap(map: BuilderMap,
348363
element: HTMLElement,
349364
key: string,
350-
input?: UpdateCallback | ClearCallback): void {
365+
input?: Builder): void {
351366
if (input !== undefined) {
352-
let oldMap = map.get(element);
353-
if (!oldMap) {
354-
oldMap = new Map();
355-
map.set(element, oldMap);
356-
}
367+
const oldMap = map.get(element) ?? new Map();
357368
oldMap.set(key, input);
369+
map.set(element, oldMap);
358370
}
359371
}
360372

projects/libs/flex-layout/extended/style/style.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export class StyleDirective extends BaseDirective2 implements DoCheck {
6262
this.ngStyleInstance = new NgStyle(elementRef, differs, renderer2);
6363
}
6464
this.init();
65-
const styles = this.nativeElement.getAttribute('style') || '';
65+
const styles = this.nativeElement.getAttribute('style') ?? '';
6666
this.fallbackStyles = this.buildStyleMap(styles);
6767
this.isServer = serverLoaded && isPlatformServer(platformId);
6868
}
@@ -92,7 +92,7 @@ export class StyleDirective extends BaseDirective2 implements DoCheck {
9292
protected buildStyleMap(styles: NgStyleType): NgStyleMap {
9393
// Always safe-guard (aka sanitize) style property values
9494
const sanitizer: NgStyleSanitizer = (val: any) =>
95-
this.sanitizer.sanitize(SecurityContext.STYLE, val) || '';
95+
this.sanitizer.sanitize(SecurityContext.STYLE, val) ?? '';
9696
if (styles) {
9797
switch (getType(styles)) {
9898
case 'string': return buildMapFromList(buildRawList(styles),

projects/libs/flex-layout/server/server-match-media.ts

+3-17
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,13 @@ export class ServerMediaQueryList implements MediaQueryList {
7878
}
7979

8080
/** Don't need to remove listeners in the server environment */
81-
removeListener(_: MediaQueryListListener | null) {
81+
removeListener() {
8282
}
8383

84-
addEventListener<K extends keyof
85-
MediaQueryListEventMap>(_: K,
86-
__: (this: MediaQueryList,
87-
ev: MediaQueryListEventMap[K]) => any,
88-
___?: boolean | AddEventListenerOptions): void;
89-
addEventListener(_: string,
90-
__: EventListenerOrEventListenerObject,
91-
___?: boolean | AddEventListenerOptions) {
84+
addEventListener() {
9285
}
9386

94-
removeEventListener<K extends keyof
95-
MediaQueryListEventMap>(_: K,
96-
__: (this: MediaQueryList,
97-
ev: MediaQueryListEventMap[K]) => any,
98-
___?: boolean | EventListenerOptions): void;
99-
removeEventListener(_: string,
100-
__: EventListenerOrEventListenerObject,
101-
___?: boolean | EventListenerOptions) {
87+
removeEventListener() {
10288
}
10389

10490
dispatchEvent(_: Event): boolean {

0 commit comments

Comments
 (0)