Skip to content

Commit 0929c2f

Browse files
authored
fix(remove httpclientmodule): refactor to drop depedency on httpClien… (#182)
* fix(remove httpclientmodule): refactor to drop depedency on httpClientModule This refactor takes care of the httpClientModule, by replacing it with a fetchHttp from our own making. * feat(transferstate): refactor to use a global, instead of parsion on intial page update the tranferstate to prepare it for external JSON, and a little optimized flow * improvement(transer-state.service): use index.html to prevent unneeded 301 read the index.html instead of the urls itself. Saves a round-trip to the server. * fix(transferstate): make sure there are no emmisions of state _during_ routing Make sure state is only emitted after routing has finished, to make sure the state doesn't get transfered to the old page. * improvement(transerstate): make sure state doesn't get emmitted during navigation This makes sure the state doesn't get emmited before navigation is done. * fix(transferstate): move navigationDome to correct place * improvement(transferstateservice): don't emit state during navigation with this, the getstate will _never_ emit a value **during** navigation. Only when navigation is done, the state will be available.
1 parent 213247f commit 0929c2f

File tree

11 files changed

+138
-134
lines changed

11 files changed

+138
-134
lines changed

extraPlugin/newSample.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
const {routeSplit, registerPlugin, httpGet} = require('../dist/scully');
2+
3+
const newsSamplePlugin = async (route, config) => {
4+
const {createPath} = routeSplit(route);
5+
const list = await httpGet('http://localhost:4200/assets/news.json');
6+
const handledRoutes = [];
7+
for (item of list) {
8+
const blogData = await httpget(`http://localhost:4200/assets/news/${list.id}.json`);
9+
handledRoutes.push({
10+
route: createPath(item.id, blogdata.slug),
11+
title: blogData.title,
12+
description: blogData.short,
13+
});
14+
}
15+
};
16+
17+
registerPlugin('router', 'myBlog', newsSamplePlugin);
18+
19+
const config = {
20+
'/news/:id/:slug': {
21+
type: 'myBlog',
22+
postRenderers: postRenderers,
23+
},
24+
};
25+
26+
/**
27+
*
28+
'/news/:id/:slug': {
29+
type: 'json',
30+
postRenderers: postRenderers,
31+
id: {
32+
url: 'http://localhost:4200/assets/news.json',
33+
property: 'id',
34+
},
35+
slug: {
36+
url: 'http://localhost:4200/assets/news/${id}.json',
37+
property: 'slug',
38+
},
39+
},
40+
41+
{
42+
"id": 5,
43+
"slug": "newsitem-5",
44+
"title": "Newsitem #5",
45+
"short": "Lorem ipsum dolor .."
46+
}
47+
*/

projects/scullyio/ng-lib/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@scullyio/ng-lib",
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"repository": {
55
"type": "GIT",
66
"url": "https://github.com/scullyio/scully/tree/master/projects/scullyio/ng-lib"
Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
import {HttpClient} from '@angular/common/http';
2-
import {ModuleWithProviders, NgModule} from '@angular/core';
1+
import {NgModule} from '@angular/core';
32
import {ScullyContentComponent} from './scully-content/scully-content.component';
43

54
@NgModule({
65
declarations: [ScullyContentComponent],
76
exports: [ScullyContentComponent],
87
})
9-
export class ComponentsModule {
10-
static forRoot(): ModuleWithProviders<ComponentsModule> {
11-
return {
12-
ngModule: ComponentsModule,
13-
providers: [HttpClient],
14-
};
15-
}
16-
}
8+
export class ComponentsModule {}

projects/scullyio/ng-lib/src/lib/route-service/scully-routes.service.spec.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
1-
import {
2-
HttpClientTestingModule,
3-
HttpTestingController,
4-
} from '@angular/common/http/testing';
5-
import { TestBed } from '@angular/core/testing';
1+
import {TestBed} from '@angular/core/testing';
62

7-
import { ScullyRoutesService } from './scully-routes.service';
3+
import {ScullyRoutesService} from './scully-routes.service';
84

95
describe('ScullyRoutesService', () => {
106
let service: ScullyRoutesService;
11-
let httpTestingController: HttpTestingController;
127

138
beforeEach(() => {
14-
TestBed.configureTestingModule({
15-
imports: [
16-
HttpClientTestingModule,
17-
],
18-
});
9+
TestBed.configureTestingModule({});
1910
service = TestBed.inject(ScullyRoutesService);
20-
httpTestingController = TestBed.inject(HttpTestingController);
2111
});
2212

2313
it('should be created', () => {

projects/scullyio/ng-lib/src/lib/route-service/scully-routes.service.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import {HttpClient} from '@angular/common/http';
21
import {Injectable} from '@angular/core';
3-
import {of, ReplaySubject, Observable} from 'rxjs';
4-
import {catchError, shareReplay, switchMap, map, tap} from 'rxjs/operators';
2+
import {Observable, of, ReplaySubject} from 'rxjs';
3+
import {catchError, map, shareReplay, switchMap} from 'rxjs/operators';
4+
import {fetchHttp} from '../utils/fetchHttp';
55

66
export interface ScullyRoute {
77
route: string;
@@ -16,7 +16,7 @@ export interface ScullyRoute {
1616
export class ScullyRoutesService {
1717
private refresh = new ReplaySubject<void>(1);
1818
available$: Observable<ScullyRoute[]> = this.refresh.pipe(
19-
switchMap(() => this.http.get<ScullyRoute[]>('/assets/scully-routes.json')),
19+
switchMap(() => fetchHttp<ScullyRoute[]>('/assets/scully-routes.json')),
2020
catchError(() => {
2121
console.warn('Scully routes file not found, are you running the in static version of your site?');
2222
return of([] as ScullyRoute[]);
@@ -29,7 +29,7 @@ export class ScullyRoutesService {
2929
shareReplay({refCount: false, bufferSize: 1})
3030
);
3131

32-
constructor(private http: HttpClient) {
32+
constructor() {
3333
/** kick off first cycle */
3434
this.reload();
3535
}

projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.spec.ts

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
1-
import {
2-
HttpClientTestingModule,
3-
HttpTestingController,
4-
} from '@angular/common/http/testing';
5-
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
6-
import { RouterTestingModule } from '@angular/router/testing';
7-
8-
import { ScullyContentComponent } from './scully-content.component';
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {RouterTestingModule} from '@angular/router/testing';
3+
import {ScullyContentComponent} from './scully-content.component';
94

105
describe('ScullyContentComponent', () => {
116
let component: ScullyContentComponent;
127
let fixture: ComponentFixture<ScullyContentComponent>;
13-
let httpTestingController: HttpTestingController;
148

159
beforeEach(async(() => {
1610
TestBed.configureTestingModule({
1711
declarations: [ScullyContentComponent],
18-
imports: [
19-
HttpClientTestingModule,
20-
RouterTestingModule.withRoutes([]),
21-
],
22-
})
23-
.compileComponents();
12+
imports: [RouterTestingModule.withRoutes([])],
13+
}).compileComponents();
2414
}));
2515

2616
beforeEach(() => {
27-
httpTestingController = TestBed.inject(HttpTestingController);
2817
fixture = TestBed.createComponent(ScullyContentComponent);
2918
component = fixture.componentInstance;
3019
fixture.detectChanges();

projects/scullyio/ng-lib/src/lib/scully-content/scully-content.component.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {HttpClient} from '@angular/common/http';
21
import {
32
ChangeDetectionStrategy,
43
Component,
@@ -13,6 +12,7 @@ import {Observable, Subscription} from 'rxjs';
1312
import {take} from 'rxjs/operators';
1413
import {IdleMonitorService} from '../idleMonitor/idle-monitor.service';
1514
import {ScullyRoutesService} from '../route-service/scully-routes.service';
15+
import {fetchHttp} from '../utils/fetchHttp';
1616

1717
/** this is needed, because otherwise the CLI borks while building */
1818
const scullyBegin = '<!--scullyContent-begin-->';
@@ -45,7 +45,6 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
4545
constructor(
4646
private elmRef: ElementRef,
4747
private srs: ScullyRoutesService,
48-
private http: HttpClient,
4948
private router: Router,
5049
private idle: IdleMonitorService
5150
) {}
@@ -67,16 +66,13 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
6766
template.innerHTML = window['scullyContent'];
6867
} else {
6968
const curPage = location.href;
70-
await this.http
71-
.get(curPage, {responseType: 'text'})
72-
.toPromise()
69+
await fetchHttp(curPage, 'text')
7370
.then((html: string) => {
7471
try {
7572
template.innerHTML = html.split(scullyBegin)[1].split(scullyEnd)[0];
7673
} catch (e) {
7774
template.innerHTML = `<h2>Sorry, could not parse static page content</h2>
7875
<p>This might happen if you are not using the static generated pages.</p>`;
79-
console.error('problem during parsing static scully content', e);
8076
}
8177
})
8278
.catch(e => {
Lines changed: 56 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
import {HttpClient} from '@angular/common/http';
2-
import {Inject, Injectable} from '@angular/core';
31
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';
57
import {isScullyGenerated, isScullyRunning} from '../utils/isScully';
6-
import {Observable, of, Subject} from 'rxjs';
7-
import {catchError, filter, map, switchMap, tap} from 'rxjs/operators';
88

99
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___ */`;
1212

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.
1417
// https://github.com/angular/angular/issues/20351#issuecomment-344009887
1518
/** @dynamic */
1619
@Injectable({
1720
providedIn: 'root',
1821
})
1922
export class TransferStateService {
2023
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+
);
2329

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) {
2931
this.setupEnvForTransferState();
3032
this.setupNavStartDataFetching();
3133
}
@@ -35,32 +37,28 @@ export class TransferStateService {
3537
// In Scully puppeteer
3638
this.script = this.document.createElement('script');
3739
this.script.setAttribute('id', SCULLY_SCRIPT_ID);
38-
this.script.setAttribute('type', `text/${SCULLY_SCRIPT_ID}`);
3940
this.document.head.appendChild(this.script);
4041
} else if (isScullyGenerated()) {
4142
// 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]) || {});
4844
}
4945
}
5046

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+
*/
5152
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));
5754
}
5855

5956
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);
6159
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
6462
)}${SCULLY_STATE_END}`;
6563
}
6664
}
@@ -69,69 +67,47 @@ export class TransferStateService {
6967
/**
7068
* Each time the route changes, get the Scully state from the server-rendered page
7169
*/
72-
if (!isScullyGenerated()) return;
70+
if (!isScullyGenerated()) {
71+
return;
72+
}
7373

7474
this.router.events
7575
.pipe(
7676
filter(e => e instanceof NavigationStart),
77-
tap(() => (this.fetching = new Subject<any>())),
7877
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 => {
8287
console.warn('Failed transfering state from route', err);
83-
return of('');
84-
})
85-
);
88+
return '';
89+
}),
90+
]);
8691
}),
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 {
9598
return null;
9699
}
97100
}),
101+
/** prevent progressing in case anything went sour above */
98102
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);
103109
})
104110
)
105111
.subscribe();
106112
}
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]);
137113
}

0 commit comments

Comments
 (0)