Skip to content

Commit

Permalink
fix(remove httpclientmodule): refactor to drop depedency on httpClien… (
Browse files Browse the repository at this point in the history
#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.
  • Loading branch information
SanderElias authored Jan 15, 2020
1 parent 213247f commit 0929c2f
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 134 deletions.
47 changes: 47 additions & 0 deletions extraPlugin/newSample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const {routeSplit, registerPlugin, httpGet} = require('../dist/scully');

const newsSamplePlugin = async (route, config) => {
const {createPath} = routeSplit(route);
const list = await httpGet('http://localhost:4200/assets/news.json');
const handledRoutes = [];
for (item of list) {
const blogData = await httpget(`http://localhost:4200/assets/news/${list.id}.json`);
handledRoutes.push({
route: createPath(item.id, blogdata.slug),
title: blogData.title,
description: blogData.short,
});
}
};

registerPlugin('router', 'myBlog', newsSamplePlugin);

const config = {
'/news/:id/:slug': {
type: 'myBlog',
postRenderers: postRenderers,
},
};

/**
*
'/news/:id/:slug': {
type: 'json',
postRenderers: postRenderers,
id: {
url: 'http://localhost:4200/assets/news.json',
property: 'id',
},
slug: {
url: 'http://localhost:4200/assets/news/${id}.json',
property: 'slug',
},
},
{
"id": 5,
"slug": "newsitem-5",
"title": "Newsitem #5",
"short": "Lorem ipsum dolor .."
}
*/
2 changes: 1 addition & 1 deletion projects/scullyio/ng-lib/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@scullyio/ng-lib",
"version": "0.0.8",
"version": "0.0.9",
"repository": {
"type": "GIT",
"url": "https://github.com/scullyio/scully/tree/master/projects/scullyio/ng-lib"
Expand Down
12 changes: 2 additions & 10 deletions projects/scullyio/ng-lib/src/lib/components.module.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import {HttpClient} from '@angular/common/http';
import {ModuleWithProviders, NgModule} from '@angular/core';
import {NgModule} from '@angular/core';
import {ScullyContentComponent} from './scully-content/scully-content.component';

@NgModule({
declarations: [ScullyContentComponent],
exports: [ScullyContentComponent],
})
export class ComponentsModule {
static forRoot(): ModuleWithProviders<ComponentsModule> {
return {
ngModule: ComponentsModule,
providers: [HttpClient],
};
}
}
export class ComponentsModule {}
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import {TestBed} from '@angular/core/testing';

import { ScullyRoutesService } from './scully-routes.service';
import {ScullyRoutesService} from './scully-routes.service';

describe('ScullyRoutesService', () => {
let service: ScullyRoutesService;
let httpTestingController: HttpTestingController;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
});
TestBed.configureTestingModule({});
service = TestBed.inject(ScullyRoutesService);
httpTestingController = TestBed.inject(HttpTestingController);
});

it('should be created', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {of, ReplaySubject, Observable} from 'rxjs';
import {catchError, shareReplay, switchMap, map, tap} from 'rxjs/operators';
import {Observable, of, ReplaySubject} from 'rxjs';
import {catchError, map, shareReplay, switchMap} from 'rxjs/operators';
import {fetchHttp} from '../utils/fetchHttp';

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

constructor(private http: HttpClient) {
constructor() {
/** kick off first cycle */
this.reload();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { ScullyContentComponent } from './scully-content.component';
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {ScullyContentComponent} from './scully-content.component';

describe('ScullyContentComponent', () => {
let component: ScullyContentComponent;
let fixture: ComponentFixture<ScullyContentComponent>;
let httpTestingController: HttpTestingController;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ScullyContentComponent],
imports: [
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
],
})
.compileComponents();
imports: [RouterTestingModule.withRoutes([])],
}).compileComponents();
}));

beforeEach(() => {
httpTestingController = TestBed.inject(HttpTestingController);
fixture = TestBed.createComponent(ScullyContentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {HttpClient} from '@angular/common/http';
import {
ChangeDetectionStrategy,
Component,
Expand All @@ -13,6 +12,7 @@ import {Observable, Subscription} from 'rxjs';
import {take} from 'rxjs/operators';
import {IdleMonitorService} from '../idleMonitor/idle-monitor.service';
import {ScullyRoutesService} from '../route-service/scully-routes.service';
import {fetchHttp} from '../utils/fetchHttp';

/** this is needed, because otherwise the CLI borks while building */
const scullyBegin = '<!--scullyContent-begin-->';
Expand Down Expand Up @@ -45,7 +45,6 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
constructor(
private elmRef: ElementRef,
private srs: ScullyRoutesService,
private http: HttpClient,
private router: Router,
private idle: IdleMonitorService
) {}
Expand All @@ -67,16 +66,13 @@ export class ScullyContentComponent implements OnInit, OnDestroy {
template.innerHTML = window['scullyContent'];
} else {
const curPage = location.href;
await this.http
.get(curPage, {responseType: 'text'})
.toPromise()
await fetchHttp(curPage, 'text')
.then((html: string) => {
try {
template.innerHTML = html.split(scullyBegin)[1].split(scullyEnd)[0];
} catch (e) {
template.innerHTML = `<h2>Sorry, could not parse static page content</h2>
<p>This might happen if you are not using the static generated pages.</p>`;
console.error('problem during parsing static scully content', e);
}
})
.catch(e => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
import {HttpClient} from '@angular/common/http';
import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {NavigationStart, Router} from '@angular/router';
import {Inject, Injectable} from '@angular/core';
import {NavigationEnd, NavigationStart, Router} from '@angular/router';
import {BehaviorSubject, EMPTY, forkJoin, Observable} from 'rxjs';
import {filter, first, map, pluck, switchMap, tap} from 'rxjs/operators';
import {fetchHttp} from '../utils/fetchHttp';
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___`;
const SCULLY_STATE_START = `/** ___SCULLY_STATE_START___ */`;
const SCULLY_STATE_END = `/** ___SCULLY_STATE_END___ */`;

// Adding this dynamic comment to supress ngc error around Document as a DI token.
interface State {
[key: string]: any;
}
// Adding this dynamic comment to suppress ngc error around Document as a DI token.
// https://github.com/angular/angular/issues/20351#issuecomment-344009887
/** @dynamic */
@Injectable({
providedIn: 'root',
})
export class TransferStateService {
private script: HTMLScriptElement;
private state: {[key: string]: any} = {};
private fetching: Subject<any>;
private isNavigatingBS = new BehaviorSubject<boolean>(false);
private stateBS = new BehaviorSubject<State>({});
private state$ = this.isNavigatingBS.pipe(
switchMap(isNav => (isNav ? EMPTY : this.stateBS.asObservable()))
);

constructor(
@Inject(DOCUMENT) private document: Document,
private router: Router,
private http: HttpClient
) {
constructor(@Inject(DOCUMENT) private document: Document, private router: Router) {
this.setupEnvForTransferState();
this.setupNavStartDataFetching();
}
Expand All @@ -35,32 +37,28 @@ export class TransferStateService {
// 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 = {};
}
this.stateBS.next((window && window[SCULLY_SCRIPT_ID]) || {});
}
}

/**
* Getstate will return an observable that fires once and completes.
* It does so right after the navigation for the page has finished.
* @param name The name of the state to
*/
getState<T>(name: string): Observable<T> {
if (this.fetching) {
return this.fetching.pipe(map(() => this.state[name]));
} else {
return of(this.state[name]);
}
return this.state$.pipe(pluck(name));
}

setState<T>(name: string, val: T): void {
this.state[name] = val;
const newState = {...this.stateBS.value, [name]: val};
this.stateBS.next(newState);
if (isScullyRunning()) {
this.script.textContent = `${SCULLY_STATE_START}${escapeHtml(
JSON.stringify(this.state)
this.script.textContent = `window['${SCULLY_SCRIPT_ID}']=${SCULLY_STATE_START}${JSON.stringify(
newState
)}${SCULLY_STATE_END}`;
}
}
Expand All @@ -69,69 +67,47 @@ export class TransferStateService {
/**
* Each time the route changes, get the Scully state from the server-rendered page
*/
if (!isScullyGenerated()) return;
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 => {
this.isNavigatingBS.next(true);
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 of('');
})
);
return '';
}),
]);
}),
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 {
/** parse out the relevant piece off text, and conver to json */
map(([e, html]: [any, string]) => {
try {
const newStateStr = html.split(SCULLY_STATE_START)[1].split(SCULLY_STATE_END)[0];
return JSON.parse(newStateStr);
} catch {
return null;
}
}),
/** prevent progressing in case anything went sour above */
filter(val => val !== null),
tap(val => {
// Add parsed-out scully-state to the current scully-state
this.setFetchedRouteState(val);
this.fetching = null;
/** activate the new state */
tap(newState => {
/** signal to send out update */
this.isNavigatingBS.next(false);
/** replace the state, so we don't leak memory on old state */
this.stateBS.next(newState);
})
)
.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]);
}
Loading

0 comments on commit 0929c2f

Please sign in to comment.