Skip to content

Commit 1461e5c

Browse files
authored
feat(transferstateservice): add support fo transfering state from bui… (#138)
* feat(transferstateservice): add support fo transfering state from buildtime to runtime TransferStateService allows you to set state and scully build time that can be used at run time in the browser. It also supports loading the state on subsequent route changes AFTER the initial page load. When you nav from one route to another, TransferStateService fetching the next route's data for you.
1 parent 8a5d741 commit 1461e5c

4 files changed

Lines changed: 174 additions & 23 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import {HttpClient} from '@angular/common/http';
2+
import {Inject, Injectable} from '@angular/core';
3+
import {DOCUMENT} from '@angular/common';
4+
import {NavigationStart, Router} from '@angular/router';
5+
import {isScullyGenerated, isScullyRunning} from '../utils/isScully';
6+
import {Observable, of, Subject} from 'rxjs';
7+
import {catchError, filter, map, switchMap, tap} from 'rxjs/operators';
8+
9+
const SCULLY_SCRIPT_ID = `scully-transfer-state`;
10+
const SCULLY_STATE_START = `___SCULLY_STATE_START___`;
11+
const SCULLY_STATE_END = `___SCULLY_STATE_END___`;
12+
13+
@Injectable({
14+
providedIn: 'root',
15+
})
16+
export class TransferStateService {
17+
private script: HTMLScriptElement;
18+
private state: {[key: string]: any} = {};
19+
private fetching: Subject<any>;
20+
21+
constructor(
22+
@Inject(DOCUMENT) private document: Document,
23+
private router: Router,
24+
private http: HttpClient
25+
) {
26+
this.setupEnvForTransferState();
27+
this.setupNavStartDataFetching();
28+
}
29+
30+
private setupEnvForTransferState(): void {
31+
if (isScullyRunning()) {
32+
// In Scully puppeteer
33+
this.script = this.document.createElement('script');
34+
this.script.setAttribute('id', SCULLY_SCRIPT_ID);
35+
this.script.setAttribute('type', `text/${SCULLY_SCRIPT_ID}`);
36+
this.document.head.appendChild(this.script);
37+
} else if (isScullyGenerated()) {
38+
// On the client AFTER scully rendered it
39+
this.script = this.document.getElementById(SCULLY_SCRIPT_ID) as HTMLScriptElement;
40+
try {
41+
this.state = JSON.parse(unescapeHtml(this.script.textContent));
42+
} catch (e) {
43+
this.state = {};
44+
}
45+
}
46+
}
47+
48+
getState<T>(name: string): Observable<T> {
49+
if (this.fetching) {
50+
return this.fetching.pipe(map(() => this.state[name]));
51+
} else {
52+
return of(this.state[name]);
53+
}
54+
}
55+
56+
setState<T>(name: string, val: T): void {
57+
this.state[name] = val;
58+
if (isScullyRunning()) {
59+
this.script.textContent = `${SCULLY_STATE_START}${escapeHtml(
60+
JSON.stringify(this.state)
61+
)}${SCULLY_STATE_END}`;
62+
}
63+
}
64+
65+
setupNavStartDataFetching() {
66+
/**
67+
* Each time the route changes, get the Scully state from the server-rendered page
68+
*/
69+
if (!isScullyGenerated()) return;
70+
71+
this.router.events
72+
.pipe(
73+
filter(e => e instanceof NavigationStart),
74+
tap(() => (this.fetching = new Subject<any>())),
75+
switchMap((e: NavigationStart) => {
76+
// Get the next route's page from the server
77+
return this.http.get(e.url, {responseType: 'text'}).pipe(
78+
catchError(err => {
79+
console.warn('Failed transfering state from route', err);
80+
return of('');
81+
})
82+
);
83+
}),
84+
map((html: string) => {
85+
// Parse the scully state out of the next page
86+
const startIndex = html.indexOf(SCULLY_STATE_START);
87+
if (startIndex !== -1) {
88+
const afterStart = html.split(SCULLY_STATE_START)[1] || '';
89+
const middle = afterStart.split(SCULLY_STATE_END)[0] || '';
90+
return middle;
91+
} else {
92+
return null;
93+
}
94+
}),
95+
filter(val => val !== null),
96+
tap(val => {
97+
// Add parsed-out scully-state to the current scully-state
98+
this.setFetchedRouteState(val);
99+
this.fetching = null;
100+
})
101+
)
102+
.subscribe();
103+
}
104+
105+
private setFetchedRouteState(unprocessedTextContext) {
106+
// Exit if nothing to set
107+
if (!unprocessedTextContext || !unprocessedTextContext.length) return;
108+
109+
// Parse to JSON the next route's state content
110+
const newState = JSON.parse(unescapeHtml(unprocessedTextContext));
111+
this.state = {...this.state, ...newState};
112+
this.fetching.next();
113+
}
114+
}
115+
export function unescapeHtml(text: string): string {
116+
const unescapedText: {[k: string]: string} = {
117+
'&a;': '&',
118+
'&q;': '"',
119+
'&s;': "'",
120+
'&l;': '<',
121+
'&g;': '>',
122+
};
123+
return text.replace(/&[^;]+;/g, s => unescapedText[s]);
124+
}
125+
export function escapeHtml(text: string): string {
126+
const escapedText: {[k: string]: string} = {
127+
'&': '&a;',
128+
'"': '&q;',
129+
"'": '&s;',
130+
'<': '&l;',
131+
'>': '&g;',
132+
};
133+
return text.replace(/[&"'<>]/g, s => escapedText[s]);
134+
}

projects/scullyio/ng-lib/src/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
export * from './lib/components.module';
66
export * from './lib/idleMonitor/idle-monitor.service';
7+
export * from './lib/transfer-state/transfer-state.service';
78
export * from './lib/route-service/scully-routes.service';
89
export * from './lib/scully-content/scully-content.component';
910
export * from './lib/utils/isScully';
10-

schematics/scully/src/ng-add/index.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ export default function(options: Schema): Rule {
2929
if (polyfills.includes('SCULLY IMPORTS')) {
3030
context.logger.info('⚠️️ Skipping polyfills.ts');
3131
} else {
32-
polyfills =
33-
polyfills +
34-
`\n/***************************************************************************************************
32+
polyfills = `${polyfills}\n/***************************************************************************************************
3533
\n* SCULLY IMPORTS
3634
\n*/
3735
\n// tslint:disable-next-line: align \nimport 'zone.js/dist/task-tracking';`;
@@ -44,10 +42,10 @@ export default function(options: Schema): Rule {
4442
if (appComponent.includes('IdleMonitorService')) {
4543
context.logger.info('⚠️️ Skipping ./src/app/app.component.ts');
4644
} else {
47-
const idleImport = "import {IdleMonitorService} from '@scullyio/ng-lib';";
45+
const idleImport = "import {IdleMonitorService, TransferStateService} from '@scullyio/ng-lib';";
4846
// add
4947
const idImport = `${idleImport} \n ${appComponent}`;
50-
const idle = 'private idle: IdleMonitorService';
48+
const idle = 'private idle: IdleMonitorService, private transferState: TransferStateService';
5149
let output = '';
5250
// check if exist
5351
if (idImport.search(/constructor/).toString() === '-1') {
@@ -79,12 +77,10 @@ export default function(options: Schema): Rule {
7977
return '';
8078
}
8179

82-
8380
} catch (e) {
8481
console.log('error in idle service');
8582
}
8683

87-
8884
const nextRules: Rule[] = [];
8985
// tslint:disable-next-line:triple-equals
9086
if (options.blog === true) {

scully/package-lock.json

Lines changed: 36 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)