diff --git a/.gitignore b/.gitignore index af69e2524..53a83251c 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,5 @@ projects/sampleBlog/src/assets/scully-routes.json scully/node_modules scully/.vscode/settings.json gp.js +cypress/screenshots +cypress/videos \ No newline at end of file diff --git a/cypress/videos/sampleBlog.spec.js.mp4 b/cypress/videos/sampleBlog.spec.js.mp4 deleted file mode 100644 index 666bab834..000000000 Binary files a/cypress/videos/sampleBlog.spec.js.mp4 and /dev/null differ diff --git a/projects/scullyio/ng-lib/src/lib/transfer-state/transfer-state.service.ts b/projects/scullyio/ng-lib/src/lib/transfer-state/transfer-state.service.ts index 212754ce0..62130db40 100644 --- a/projects/scullyio/ng-lib/src/lib/transfer-state/transfer-state.service.ts +++ b/projects/scullyio/ng-lib/src/lib/transfer-state/transfer-state.service.ts @@ -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; @@ -21,17 +33,42 @@ interface State { }) export class TransferStateService { private script: HTMLScriptElement; - // private stateBS = new BehaviorSubject({}); - /** 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({}); 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 { @@ -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(name: string): Observable { + /** start of the fetch for the current active route. */ + this.fetchTransferState(); return this.state$.pipe(pluck(name)); } @@ -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 { + /** 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(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(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); @@ -125,6 +165,12 @@ export class TransferStateService { this.stateBS.next(newState); }) ) - .subscribe(); + .subscribe({ + /** when completes (different URL) */ + complete: () => { + /** reset the currentBaseUrl */ + this.currentBaseUrl = '//'; + }, + }); } } diff --git a/src/__tests__/__snapshots__/blog-index.spec.ts.snap b/src/__tests__/__snapshots__/blog-index.spec.ts.snap index 3043627a6..7329561c1 100644 --- a/src/__tests__/__snapshots__/blog-index.spec.ts.snap +++ b/src/__tests__/__snapshots__/blog-index.spec.ts.snap @@ -7,7 +7,51 @@ exports[`ContentFolder: Test blog/page-1 Check contentPlugin render 1`] = ` - - Scully demo blog app! rendering inside scully🏠

Scully blog content

+ Scully demo blog app! rendering inside scully🏠

Scully blog content


@@ -32,8 +76,8 @@ console.log('yah');

End of blog content

-

Made with ❤️ @HeroDevs

- +

Made with ❤️ @HeroDevs

+ " `; @@ -45,11 +89,65 @@ exports[`Static: Test blog index Check clean blog index by scully 1`] = ` - + - Scully demo blog app! rendering inside scully🏠

Overview of blog posts

My first page

This is the first demo page in this sample.

My third page

At this point, I should write something different in here.

The Rainbow Cat, A free book out of the Guttenberg library

Made with ❤️ @HeroDevs

- + Scully demo blog app! rendering inside scully🏠

Overview of blog posts

My first page

This is the first demo page in this sample.

My third page

At this point, I should write something different in here.

The Rainbow Cat, A free book out of the Guttenberg library

Made with ❤️ @HeroDevs

+ " `; diff --git a/src/__tests__/__snapshots__/home.spec.ts.snap b/src/__tests__/__snapshots__/home.spec.ts.snap index 3950b37a3..c8a3fb3a0 100644 --- a/src/__tests__/__snapshots__/home.spec.ts.snap +++ b/src/__tests__/__snapshots__/home.spec.ts.snap @@ -7,11 +7,81 @@ exports[`Check list of all Check clean all list from scully 1`] = ` - + - Scully demo blog app! rendering inside scully🏠

Available routes

Top level routes onlyUnpublished routes

Made with ❤️ @HeroDevs

- + Scully demo blog app! rendering inside scully🏠

Available routes

Top level routes onlyUnpublished routes

Made with ❤️ @HeroDevs

+ " `; diff --git a/src/__tests__/__snapshots__/users.spec.ts.snap b/src/__tests__/__snapshots__/users.spec.ts.snap index 94691ab27..a528e2c5b 100644 --- a/src/__tests__/__snapshots__/users.spec.ts.snap +++ b/src/__tests__/__snapshots__/users.spec.ts.snap @@ -7,11 +7,51 @@ exports[`JsonPlugin: test user List Check clean blog index by scully 1`] = ` - + - Scully demo blog app! rendering inside scully🏠

Users

IdUsername
1 Leanne Graham
2 Ervin Howell
3 Clementine Bauch
4 Patricia Lebsack
5 Chelsey Dietrich
6 Mrs. Dennis Schulist
7 Kurtis Weissnat
8 Nicholas Runolfsdottir V
9 Glenna Reichert
10 Clementina DuBuque

Made with ❤️ @HeroDevs

- + Scully demo blog app! rendering inside scully🏠

Users

IdUsername
1 Leanne Graham
2 Ervin Howell
3 Clementine Bauch
4 Patricia Lebsack
5 Chelsey Dietrich
6 Mrs. Dennis Schulist
7 Kurtis Weissnat
8 Nicholas Runolfsdottir V
9 Glenna Reichert
10 Clementina DuBuque

Made with ❤️ @HeroDevs

+ " `;