1
- import { HttpClient } from '@angular/common/http' ;
2
- import { Inject , Injectable } from '@angular/core' ;
3
1
import { DOCUMENT } from '@angular/common' ;
4
- import { NavigationStart , Router } from '@angular/router' ;
2
+ import { Inject , Injectable } from '@angular/core' ;
3
+ import { NavigationEnd , NavigationStart , Router } from '@angular/router' ;
4
+ import { BehaviorSubject , EMPTY , forkJoin , Observable } from 'rxjs' ;
5
+ import { filter , first , map , pluck , switchMap , tap } from 'rxjs/operators' ;
6
+ import { fetchHttp } from '../utils/fetchHttp' ;
5
7
import { isScullyGenerated , isScullyRunning } from '../utils/isScully' ;
6
- import { Observable , of , Subject } from 'rxjs' ;
7
- import { catchError , filter , map , switchMap , tap } from 'rxjs/operators' ;
8
8
9
9
const SCULLY_SCRIPT_ID = `scully-transfer-state` ;
10
- const SCULLY_STATE_START = `___SCULLY_STATE_START___` ;
11
- const SCULLY_STATE_END = `___SCULLY_STATE_END___` ;
10
+ const SCULLY_STATE_START = `/** ___SCULLY_STATE_START___ */ ` ;
11
+ const SCULLY_STATE_END = `/** ___SCULLY_STATE_END___ */ ` ;
12
12
13
- // Adding this dynamic comment to supress ngc error around Document as a DI token.
13
+ interface State {
14
+ [ key : string ] : any ;
15
+ }
16
+ // Adding this dynamic comment to suppress ngc error around Document as a DI token.
14
17
// https://github.com/angular/angular/issues/20351#issuecomment-344009887
15
18
/** @dynamic */
16
19
@Injectable ( {
17
20
providedIn : 'root' ,
18
21
} )
19
22
export class TransferStateService {
20
23
private script : HTMLScriptElement ;
21
- private state : { [ key : string ] : any } = { } ;
22
- private fetching : Subject < any > ;
24
+ private isNavigatingBS = new BehaviorSubject < boolean > ( false ) ;
25
+ private stateBS = new BehaviorSubject < State > ( { } ) ;
26
+ private state$ = this . isNavigatingBS . pipe (
27
+ switchMap ( isNav => ( isNav ? EMPTY : this . stateBS . asObservable ( ) ) )
28
+ ) ;
23
29
24
- constructor (
25
- @Inject ( DOCUMENT ) private document : Document ,
26
- private router : Router ,
27
- private http : HttpClient
28
- ) {
30
+ constructor ( @Inject ( DOCUMENT ) private document : Document , private router : Router ) {
29
31
this . setupEnvForTransferState ( ) ;
30
32
this . setupNavStartDataFetching ( ) ;
31
33
}
@@ -35,32 +37,28 @@ export class TransferStateService {
35
37
// In Scully puppeteer
36
38
this . script = this . document . createElement ( 'script' ) ;
37
39
this . script . setAttribute ( 'id' , SCULLY_SCRIPT_ID ) ;
38
- this . script . setAttribute ( 'type' , `text/${ SCULLY_SCRIPT_ID } ` ) ;
39
40
this . document . head . appendChild ( this . script ) ;
40
41
} else if ( isScullyGenerated ( ) ) {
41
42
// On the client AFTER scully rendered it
42
- this . script = this . document . getElementById ( SCULLY_SCRIPT_ID ) as HTMLScriptElement ;
43
- try {
44
- this . state = JSON . parse ( unescapeHtml ( this . script . textContent ) ) ;
45
- } catch ( e ) {
46
- this . state = { } ;
47
- }
43
+ this . stateBS . next ( ( window && window [ SCULLY_SCRIPT_ID ] ) || { } ) ;
48
44
}
49
45
}
50
46
47
+ /**
48
+ * Getstate will return an observable that fires once and completes.
49
+ * It does so right after the navigation for the page has finished.
50
+ * @param name The name of the state to
51
+ */
51
52
getState < T > ( name : string ) : Observable < T > {
52
- if ( this . fetching ) {
53
- return this . fetching . pipe ( map ( ( ) => this . state [ name ] ) ) ;
54
- } else {
55
- return of ( this . state [ name ] ) ;
56
- }
53
+ return this . state$ . pipe ( pluck ( name ) ) ;
57
54
}
58
55
59
56
setState < T > ( name : string , val : T ) : void {
60
- this . state [ name ] = val ;
57
+ const newState = { ...this . stateBS . value , [ name ] : val } ;
58
+ this . stateBS . next ( newState ) ;
61
59
if ( isScullyRunning ( ) ) {
62
- this . script . textContent = `${ SCULLY_STATE_START } ${ escapeHtml (
63
- JSON . stringify ( this . state )
60
+ this . script . textContent = `window[' ${ SCULLY_SCRIPT_ID } ']= ${ SCULLY_STATE_START } ${ JSON . stringify (
61
+ newState
64
62
) } ${ SCULLY_STATE_END } `;
65
63
}
66
64
}
@@ -69,69 +67,47 @@ export class TransferStateService {
69
67
/**
70
68
* Each time the route changes, get the Scully state from the server-rendered page
71
69
*/
72
- if ( ! isScullyGenerated ( ) ) return ;
70
+ if ( ! isScullyGenerated ( ) ) {
71
+ return ;
72
+ }
73
73
74
74
this . router . events
75
75
. pipe (
76
76
filter ( e => e instanceof NavigationStart ) ,
77
- tap ( ( ) => ( this . fetching = new Subject < any > ( ) ) ) ,
78
77
switchMap ( ( e : NavigationStart ) => {
79
- // Get the next route's page from the server
80
- return this . http . get ( e . url , { responseType : 'text' } ) . pipe (
81
- catchError ( err => {
78
+ this . isNavigatingBS . next ( true ) ;
79
+ return forkJoin ( [
80
+ /** prevent emitting before navigation to _this_ URL is done. */
81
+ this . router . events . pipe (
82
+ filter ( ev => ev instanceof NavigationEnd && ev . url === e . url ) ,
83
+ first ( )
84
+ ) ,
85
+ // Get the next route's page from the server
86
+ fetchHttp < string > ( e . url + '/index.html' , 'text' ) . catch ( err => {
82
87
console . warn ( 'Failed transfering state from route' , err ) ;
83
- return of ( '' ) ;
84
- } )
85
- ) ;
88
+ return '' ;
89
+ } ) ,
90
+ ] ) ;
86
91
} ) ,
87
- map ( ( html : string ) => {
88
- // Parse the scully state out of the next page
89
- const startIndex = html . indexOf ( SCULLY_STATE_START ) ;
90
- if ( startIndex !== - 1 ) {
91
- const afterStart = html . split ( SCULLY_STATE_START ) [ 1 ] || '' ;
92
- const middle = afterStart . split ( SCULLY_STATE_END ) [ 0 ] || '' ;
93
- return middle ;
94
- } else {
92
+ /** parse out the relevant piece off text, and conver to json */
93
+ map ( ( [ e , html ] : [ any , string ] ) => {
94
+ try {
95
+ const newStateStr = html . split ( SCULLY_STATE_START ) [ 1 ] . split ( SCULLY_STATE_END ) [ 0 ] ;
96
+ return JSON . parse ( newStateStr ) ;
97
+ } catch {
95
98
return null ;
96
99
}
97
100
} ) ,
101
+ /** prevent progressing in case anything went sour above */
98
102
filter ( val => val !== null ) ,
99
- tap ( val => {
100
- // Add parsed-out scully-state to the current scully-state
101
- this . setFetchedRouteState ( val ) ;
102
- this . fetching = null ;
103
+ /** activate the new state */
104
+ tap ( newState => {
105
+ /** signal to send out update */
106
+ this . isNavigatingBS . next ( false ) ;
107
+ /** replace the state, so we don't leak memory on old state */
108
+ this . stateBS . next ( newState ) ;
103
109
} )
104
110
)
105
111
. subscribe ( ) ;
106
112
}
107
-
108
- private setFetchedRouteState ( unprocessedTextContext ) {
109
- // Exit if nothing to set
110
- if ( ! unprocessedTextContext || ! unprocessedTextContext . length ) return ;
111
-
112
- // Parse to JSON the next route's state content
113
- const newState = JSON . parse ( unescapeHtml ( unprocessedTextContext ) ) ;
114
- this . state = { ...this . state , ...newState } ;
115
- this . fetching . next ( ) ;
116
- }
117
- }
118
- export function unescapeHtml ( text : string ) : string {
119
- const unescapedText : { [ k : string ] : string } = {
120
- '&a;' : '&' ,
121
- '&q;' : '"' ,
122
- '&s;' : "'" ,
123
- '&l;' : '<' ,
124
- '&g;' : '>' ,
125
- } ;
126
- return text . replace ( / & [ ^ ; ] + ; / g, s => unescapedText [ s ] ) ;
127
- }
128
- export function escapeHtml ( text : string ) : string {
129
- const escapedText : { [ k : string ] : string } = {
130
- '&' : '&a;' ,
131
- '"' : '&q;' ,
132
- "'" : '&s;' ,
133
- '<' : '&l;' ,
134
- '>' : '&g;' ,
135
- } ;
136
- return text . replace ( / [ & " ' < > ] / g, s => escapedText [ s ] ) ;
137
113
}
0 commit comments