Skip to content
Merged
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 .."
}
*/
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,8 +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 {HandledRoute} from 'dist/scully';
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 @@ -17,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 @@ -30,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,29 @@
import {HttpClient} from '@angular/common/http';
import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {Inject, Injectable} from '@angular/core';
import {NavigationStart, Router} from '@angular/router';
import {BehaviorSubject, from, Observable, of} from 'rxjs';
import {catchError, filter, 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 state$ = new BehaviorSubject<State>({});

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 +33,23 @@ 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.state$.next((window && window[SCULLY_SCRIPT_ID]) || {});
}
}

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.state$.value, [name]: val};
this.state$.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 +58,40 @@ 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>())),
Comment thread
SanderElias marked this conversation as resolved.
switchMap((e: NavigationStart) => {
// Get the next route's page from the server
return this.http.get(e.url, {responseType: 'text'}).pipe(
return from(fetchHttp(e.url, 'text')).pipe(
catchError(err => {
console.warn('Failed transfering state from route', err);
return of('');
})
);
}),
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 {
try {
const newStateStr = html.split(SCULLY_STATE_START)[1].split(SCULLY_STATE_END)[0];
return JSON.parse(newStateStr);
} catch {
return null;
}
}),
filter(val => val !== null),
tap(val => {
tap(newState => {
// Add parsed-out scully-state to the current scully-state
this.setFetchedRouteState(val);
this.fetching = null;
this.setFetchedRouteState(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();
private setFetchedRouteState(newState) {
this.state$.next({...this.state$.value, ...newState});
}
}
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]);
}
10 changes: 10 additions & 0 deletions projects/scullyio/ng-lib/src/lib/utils/fetchHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function fetchHttp<T>(url: string, responseType: XMLHttpRequestResponseType = 'json'): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = responseType;
xhr.addEventListener('load', ev => resolve(xhr.response));
xhr.addEventListener('error', (...err) => reject(err));
xhr.open('get', url, true);
xhr.send();
});
}
4 changes: 2 additions & 2 deletions scully.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ exports.config = {
property: 'id',
},
},
'/ouser/:userId/:friendId': {
'/user/:userId/:friendCode': {
// Type is mandatory
type: 'json',
/**
Expand All @@ -42,7 +42,7 @@ exports.config = {
url: 'https://jsonplaceholder.typicode.com/users',
property: 'id',
},
friendId: {
friendCode: {
/** users are their own friend in this sample ;) */
url: 'https://jsonplaceholder.typicode.com/users?userId=${userId}',
property: 'id',
Expand Down