Skip to content

Commit

Permalink
feat(transferstateservice): add support fo transfering state from bui… (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
aaronfrost authored Jan 6, 2020
1 parent 8a5d741 commit 1461e5c
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {HttpClient} from '@angular/common/http';
import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {NavigationStart, Router} from '@angular/router';
import {isScullyGenerated, isScullyRunning} from '../utils/isScully';
import {Observable, of, Subject} from 'rxjs';
import {catchError, filter, map, switchMap, tap} from 'rxjs/operators';

const SCULLY_SCRIPT_ID = `scully-transfer-state`;
const SCULLY_STATE_START = `___SCULLY_STATE_START___`;
const SCULLY_STATE_END = `___SCULLY_STATE_END___`;

@Injectable({
providedIn: 'root',
})
export class TransferStateService {
private script: HTMLScriptElement;
private state: {[key: string]: any} = {};
private fetching: Subject<any>;

constructor(
@Inject(DOCUMENT) private document: Document,
private router: Router,
private http: HttpClient
) {
this.setupEnvForTransferState();
this.setupNavStartDataFetching();
}

private setupEnvForTransferState(): void {
if (isScullyRunning()) {
// In Scully puppeteer
this.script = this.document.createElement('script');
this.script.setAttribute('id', SCULLY_SCRIPT_ID);
this.script.setAttribute('type', `text/${SCULLY_SCRIPT_ID}`);
this.document.head.appendChild(this.script);
} else if (isScullyGenerated()) {
// On the client AFTER scully rendered it
this.script = this.document.getElementById(SCULLY_SCRIPT_ID) as HTMLScriptElement;
try {
this.state = JSON.parse(unescapeHtml(this.script.textContent));
} catch (e) {
this.state = {};
}
}
}

getState<T>(name: string): Observable<T> {
if (this.fetching) {
return this.fetching.pipe(map(() => this.state[name]));
} else {
return of(this.state[name]);
}
}

setState<T>(name: string, val: T): void {
this.state[name] = val;
if (isScullyRunning()) {
this.script.textContent = `${SCULLY_STATE_START}${escapeHtml(
JSON.stringify(this.state)
)}${SCULLY_STATE_END}`;
}
}

setupNavStartDataFetching() {
/**
* Each time the route changes, get the Scully state from the server-rendered page
*/
if (!isScullyGenerated()) return;

this.router.events
.pipe(
filter(e => e instanceof NavigationStart),
tap(() => (this.fetching = new Subject<any>())),
switchMap((e: NavigationStart) => {
// Get the next route's page from the server
return this.http.get(e.url, {responseType: 'text'}).pipe(
catchError(err => {
console.warn('Failed transfering state from route', err);
return of('');
})
);
}),
map((html: string) => {
// Parse the scully state out of the next page
const startIndex = html.indexOf(SCULLY_STATE_START);
if (startIndex !== -1) {
const afterStart = html.split(SCULLY_STATE_START)[1] || '';
const middle = afterStart.split(SCULLY_STATE_END)[0] || '';
return middle;
} else {
return null;
}
}),
filter(val => val !== null),
tap(val => {
// Add parsed-out scully-state to the current scully-state
this.setFetchedRouteState(val);
this.fetching = null;
})
)
.subscribe();
}

private setFetchedRouteState(unprocessedTextContext) {
// Exit if nothing to set
if (!unprocessedTextContext || !unprocessedTextContext.length) return;

// Parse to JSON the next route's state content
const newState = JSON.parse(unescapeHtml(unprocessedTextContext));
this.state = {...this.state, ...newState};
this.fetching.next();
}
}
export function unescapeHtml(text: string): string {
const unescapedText: {[k: string]: string} = {
'&a;': '&',
'&q;': '"',
'&s;': "'",
'&l;': '<',
'&g;': '>',
};
return text.replace(/&[^;]+;/g, s => unescapedText[s]);
}
export function escapeHtml(text: string): string {
const escapedText: {[k: string]: string} = {
'&': '&a;',
'"': '&q;',
"'": '&s;',
'<': '&l;',
'>': '&g;',
};
return text.replace(/[&"'<>]/g, s => escapedText[s]);
}
2 changes: 1 addition & 1 deletion projects/scullyio/ng-lib/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

export * from './lib/components.module';
export * from './lib/idleMonitor/idle-monitor.service';
export * from './lib/transfer-state/transfer-state.service';
export * from './lib/route-service/scully-routes.service';
export * from './lib/scully-content/scully-content.component';
export * from './lib/utils/isScully';

10 changes: 3 additions & 7 deletions schematics/scully/src/ng-add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ export default function(options: Schema): Rule {
if (polyfills.includes('SCULLY IMPORTS')) {
context.logger.info('⚠️️ Skipping polyfills.ts');
} else {
polyfills =
polyfills +
`\n/***************************************************************************************************
polyfills = `${polyfills}\n/***************************************************************************************************
\n* SCULLY IMPORTS
\n*/
\n// tslint:disable-next-line: align \nimport 'zone.js/dist/task-tracking';`;
Expand All @@ -44,10 +42,10 @@ export default function(options: Schema): Rule {
if (appComponent.includes('IdleMonitorService')) {
context.logger.info('⚠️️ Skipping ./src/app/app.component.ts');
} else {
const idleImport = "import {IdleMonitorService} from '@scullyio/ng-lib';";
const idleImport = "import {IdleMonitorService, TransferStateService} from '@scullyio/ng-lib';";
// add
const idImport = `${idleImport} \n ${appComponent}`;
const idle = 'private idle: IdleMonitorService';
const idle = 'private idle: IdleMonitorService, private transferState: TransferStateService';
let output = '';
// check if exist
if (idImport.search(/constructor/).toString() === '-1') {
Expand Down Expand Up @@ -79,12 +77,10 @@ export default function(options: Schema): Rule {
return '';
}


} catch (e) {
console.log('error in idle service');
}


const nextRules: Rule[] = [];
// tslint:disable-next-line:triple-equals
if (options.blog === true) {
Expand Down
51 changes: 36 additions & 15 deletions scully/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 1461e5c

Please sign in to comment.