Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ projects/sampleBlog/src/assets/scully-routes.json
scully/node_modules
scully/.vscode/settings.json
gp.js
cypress/screenshots
cypress/videos
Binary file removed cypress/videos/sampleBlog.spec.js.mp4
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import {DOCUMENT} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
import {NavigationEnd, NavigationStart, Router} from '@angular/router';
import {BehaviorSubject, forkJoin, NEVER, Observable, of} from 'rxjs';
import {catchError, filter, first, map, pluck, switchMap, tap} from 'rxjs/operators';
import {BehaviorSubject, NEVER, Observable, of} from 'rxjs';
import {
catchError,
filter,
first,
map,
pluck,
shareReplay,
switchMap,
take,
takeWhile,
tap,
} from 'rxjs/operators';
import {fetchHttp} from '../utils/fetchHttp';
import {isScullyGenerated, isScullyRunning} from '../utils/isScully';

const SCULLY_SCRIPT_ID = `scully-transfer-state`;
const SCULLY_STATE_START = `/** ___SCULLY_STATE_START___ */`;
const SCULLY_STATE_END = `/** ___SCULLY_STATE_END___ */`;
const initialStateDone = '__done__with__Initial__navigation__';

interface State {
[key: string]: any;
Expand All @@ -21,17 +33,42 @@ interface State {
})
export class TransferStateService {
private script: HTMLScriptElement;
// private stateBS = new BehaviorSubject<State>({});
/** subject to fire off incomming states */

private initialUrl: string;
/** set the currentBase to something that it can never be */
private currentBaseUrl = '//';
/** subject to fire off incoming states */
private stateBS = new BehaviorSubject<State>({});
private state$ = this.stateBS.pipe(filter(state => state !== undefined));
// emit the next url when routing is complete
private nextUrl = this.router.events.pipe(
filter(e => e instanceof NavigationStart),
switchMap((e: NavigationStart) => {
if (this.initialUrl === e.url) {
/** don't kick off on initial load to prevent flicker */
this.initialUrl = initialStateDone;
return NEVER;
}
return of(e);
}),
/** reset the state, so new components will never get stale data */
tap(() => this.stateBS.next(undefined)),
/** prevent emitting before navigation to _this_ URL is done. */
switchMap((e: NavigationStart) =>
this.router.events.pipe(
filter(ev => ev instanceof NavigationEnd && ev.url === e.url),
first()
)
),
map((ev: NavigationEnd) => ev.url),
shareReplay(1)
);

constructor(@Inject(DOCUMENT) private document: Document, private router: Router) {}

startMonitoring() {
this.setupEnvForTransferState();
this.setupNavStartDataFetching();
this.setupStartNavMonitoring();
}

private setupEnvForTransferState(): void {
Expand All @@ -50,12 +87,14 @@ export class TransferStateService {
}

/**
* Getstate will return an observable that fires once and completes.
* Getstate will return an observable that containes the data.
* It does so right after the navigation for the page has finished.
* please note, this works SYNC on initial route, preventing a flash of content.
* @param name The name of the state to
*/
getState<T>(name: string): Observable<T> {
/** start of the fetch for the current active route. */
this.fetchTransferState();
return this.state$.pipe(pluck(name));
}

Expand All @@ -69,43 +108,44 @@ export class TransferStateService {
}
}

setupNavStartDataFetching() {
/**
* Each time the route changes, get the Scully state from the server-rendered page
*/
/**
* starts monitoring the router, and keep the url from the last completed navigation handy.
*/
setupStartNavMonitoring() {
if (!isScullyGenerated()) {
return;
}

this.router.events
/** start monitoring the routes */
this.nextUrl.subscribe();
}

async fetchTransferState(): Promise<void> {
/** helper to read the part before the first slash (ignores leading slash) */
const base = (url: string) => url.split('/').filter(part => part.trim() !== '')[0];
/** put this in the next event cycle so the correct route can be read */
await new Promise(r => setTimeout(r, 0));
/** get the current url */
const currentUrl = await this.nextUrl.pipe(take(1)).toPromise();
const baseUrl = base(currentUrl);
if (this.currentBaseUrl === baseUrl) {
/** already monitoring, don't tho a thing */
return;
}
/** keep the baseUrl for later reference */
this.currentBaseUrl = baseUrl;
this.nextUrl
.pipe(
filter(e => e instanceof NavigationStart),
switchMap((e: NavigationStart) => {
if (this.initialUrl === e.url) {
/** don't kick off on initial load to prevent flicker */
this.initialUrl = '__done__with__Initial__navigation__';
return NEVER;
}
return of(e);
}),
/** reset the state, so new components will never get stale data */
tap(() => this.stateBS.next(undefined)),
switchMap((e: NavigationStart) => {
return forkJoin([
/** prevent emitting before navigation to _this_ URL is done. */
this.router.events.pipe(
filter(ev => ev instanceof NavigationEnd && ev.url === e.url),
first()
),
// Get the next route's page from the server
fetchHttp<string>(e.url + '/index.html', 'text').catch(err => {
console.warn('Failed transfering state from route', err);
return '';
}),
]);
}),
/** parse out the relevant piece off text, and convert to json */
map(([e, html]: [any, string]) => {
/** keep updating till we move to another route */
takeWhile(url => base(url) === this.currentBaseUrl),
switchMap(url =>
// Get the next route's page from the server
fetchHttp<string>(url + '/index.html', 'text').catch(err => {
console.warn('Failed transfering state from route', err);
return '';
})
),
map((html: string) => {
try {
const newStateStr = html.split(SCULLY_STATE_START)[1].split(SCULLY_STATE_END)[0];
return JSON.parse(newStateStr);
Expand All @@ -125,6 +165,12 @@ export class TransferStateService {
this.stateBS.next(newState);
})
)
.subscribe();
.subscribe({
/** when completes (different URL) */
complete: () => {
/** reset the currentBaseUrl */
this.currentBaseUrl = '//';
},
});
}
}
Loading