├── browser.d.ts ├── server.d.ts ├── browser.js ├── server.js ├── .gitignore ├── index.d.ts ├── tsconfig.es2015.json ├── tsconfig.json ├── src ├── browser.ts ├── server.ts └── transfer-state.ts └── package.json /browser.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/browser'; 2 | -------------------------------------------------------------------------------- /server.d.ts: -------------------------------------------------------------------------------- 1 | export * from './dist/server'; 2 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/browser'); 2 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/server'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /dist/ 3 | /node_modules/ 4 | 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/Microsoft/TypeScript/issues/7753 2 | export * from './dist/server'; 3 | -------------------------------------------------------------------------------- /tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "es2015", 5 | "baseUrl": ".", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "stripInternal": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "noImplicitAny": true, 13 | "sourceMap": true, 14 | "inlineSources": true, 15 | "skipLibCheck": true, 16 | "outDir": "dist/es2015", 17 | "rootDir": "src", 18 | "lib": ["es6", "dom"] 19 | }, 20 | "files": [ 21 | "./src/browser.ts" 22 | ], 23 | "angularCompilerOptions": { 24 | "strictMetadataEmit": true, 25 | "skipMetadataEmit" : false 26 | }, 27 | "compileOnSave": false, 28 | "buildOnSave": false, 29 | "atom": { "rewriteTsconfig": false } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "baseUrl": ".", 6 | "moduleResolution": "node", 7 | "declaration": true, 8 | "stripInternal": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "noImplicitReturns": true, 12 | "noImplicitAny": true, 13 | "sourceMap": true, 14 | "inlineSources": true, 15 | "skipLibCheck": true, 16 | "outDir": "dist", 17 | "rootDir": "src", 18 | "lib": ["es6", "dom"] 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "typings/main.d.ts", 23 | "typings/main", 24 | "browser.d.ts", 25 | "server.d.ts" 26 | ], 27 | "angularCompilerOptions": { 28 | "strictMetadataEmit": true, 29 | "skipMetadataEmit" : false 30 | }, 31 | "compileOnSave": false, 32 | "buildOnSave": false, 33 | "atom": { "rewriteTsconfig": false } 34 | } 35 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, APP_INITIALIZER } from '@angular/core'; 2 | import { TransferState } from './transfer-state'; 3 | 4 | export function getTransferState(): TransferState { 5 | const win: any = window; 6 | const transferState = new TransferState(); 7 | transferState.initialize(win[TransferState.KEY] || {}); 8 | return transferState; 9 | } 10 | 11 | export function noop() { 12 | 13 | } 14 | 15 | export function getTransferInitializer (transferState: TransferState): any { 16 | return noop; 17 | } 18 | 19 | @NgModule({ 20 | providers: [ 21 | { 22 | provide: TransferState, 23 | useFactory: getTransferState 24 | }, 25 | { 26 | provide: APP_INITIALIZER, 27 | multi: true, 28 | useFactory: getTransferInitializer, 29 | deps: [ TransferState ] 30 | } 31 | ] 32 | }) 33 | export class BrowserTransferStateModule { 34 | 35 | } 36 | 37 | export { TransferState, TransferHttp, TransferHttpModule } from './transfer-state'; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@angularclass/universal-transfer-state", 3 | "version": "2.0.2", 4 | "description": "universal-transfer-state: Angular Universal module for transfering state from server to client", 5 | "main": "./server.js", 6 | "browser": "./browser.js", 7 | "typings": "./index.d.ts", 8 | "files": [ 9 | "index.d.ts", 10 | "browser.d.ts", 11 | "browser.js", 12 | "server.d.ts", 13 | "server.js", 14 | "dist" 15 | ], 16 | "scripts": { 17 | "prebuild": "rm -rf dist", 18 | "build": "npm run build:es5 && rm -rf ./index.ngsummary.json && rm -rf ./src/*.{ngsummary,ngfactory}.{json,ts}", 19 | "build:es5": "./node_modules/.bin/ngc", 20 | "build:es2015": "node node_modules/.bin/ngc -p tsconfig.es2015.json", 21 | "test": "echo \"Error: no test specified\" && exit 1", 22 | "prepublish": "npm run build" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/angularclass/angular-universal-transfer-state.git" 27 | }, 28 | "author": "PatrickJS ", 29 | "license": "Apache-2.0", 30 | "bugs": { 31 | "url": "https://github.com/angularclass/angular-universal-transfer-state/issues" 32 | }, 33 | "homepage": "https://github.com/angularclass/angular-universal-transfer-state#readme", 34 | "devDependencies": { 35 | "@angular/animations": "~4.2.6", 36 | "@angular/common": "~4.2.6", 37 | "@angular/compiler": "~4.2.6", 38 | "@angular/compiler-cli": "~4.2.6", 39 | "@angular/core": "~4.2.6", 40 | "@angular/http": "~4.2.6", 41 | "@angular/platform-browser": "~4.2.6", 42 | "@angular/platform-browser-dynamic": "~4.2.6", 43 | "@angular/platform-server": "~4.2.6", 44 | "@angular/router": "~4.2.6", 45 | "@types/express": "^4.0.35", 46 | "@types/node": "^7.0.8", 47 | "@types/serialize-javascript": "^1.3.1", 48 | "rxjs": "^5.2.0", 49 | "typescript": "~2.3.2", 50 | "xhr2": "^0.1.4", 51 | "zone.js": "~0.8.2" 52 | }, 53 | "dependencies": { 54 | "serialize-javascript": "^1.3.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Optional, RendererFactory2, ViewEncapsulation, NgModule } from '@angular/core'; 2 | import { PlatformState } from '@angular/platform-server'; 3 | import { TransferState } from './transfer-state'; 4 | import * as serialize from 'serialize-javascript'; 5 | 6 | export function isTag(tagName: string, node: any): boolean { 7 | return node.type === 'tag' && node.name === tagName; 8 | } 9 | 10 | @Injectable() 11 | export class ServerTransferState extends TransferState { 12 | _serialize = serialize; 13 | constructor( private state: PlatformState, private rendererFactory: RendererFactory2) { 14 | super(); 15 | } 16 | 17 | /** 18 | * Serialize state to Json string 19 | */ 20 | stateToString(json: any, options: any = {isJSON: true}) { 21 | const _json = this._serialize(json, options); 22 | return _json; 23 | } 24 | 25 | getDocumentFromState(state: any = this.state) { 26 | const document: any = state.getDocument(); 27 | return document; 28 | } 29 | 30 | 31 | 32 | /** 33 | * Inject the State into the bottom of the 34 | */ 35 | inject(location?: string) { 36 | try { 37 | const document: any = this.getDocumentFromState(this.state); 38 | const json = this.toJson(); 39 | const transferStateString = this.stateToString(json); 40 | const renderer = this.rendererFactory.createRenderer(document, { 41 | id: '-1', 42 | encapsulation: ViewEncapsulation.None, 43 | styles: [], 44 | data: {} 45 | }); 46 | 47 | 48 | let rootNode: any = undefined; 49 | let bodyNode: any = undefined; 50 | let headNode: any = undefined; 51 | 52 | // let titleNode: any = undefined; 53 | 54 | for (let i: number = 0; i < document.childNodes.length; ++i) { 55 | const child = document.childNodes[i]; 56 | 57 | if (isTag('html', child)) { 58 | rootNode = child; 59 | break; 60 | } 61 | } 62 | 63 | if (!rootNode) { 64 | rootNode = document; 65 | } 66 | 67 | 68 | for (let i: number = 0; i < rootNode.childNodes.length; ++i) { 69 | const child = rootNode.childNodes[i]; 70 | 71 | if (isTag('head', child)) { 72 | headNode = child; 73 | } 74 | if (isTag('body', child)) { 75 | bodyNode = child; 76 | } 77 | } 78 | 79 | const body = location === 'head' ? headNode : bodyNode; 80 | 81 | const script = renderer.createElement('script'); 82 | renderer.setValue(script, ` 83 | try { 84 | window['${ TransferState.KEY }'] = ${ transferStateString } 85 | } catch (e) { 86 | console.log('Angular Universal: There was a problem parsing the server data during rehydrate'); 87 | } 88 | `); 89 | renderer.appendChild(body, script); 90 | renderer.setAttribute(script, 'angular', 'universal'); 91 | renderer.setAttribute(script, 'angularclass', 'universal-transfer-state'); 92 | rootNode = undefined; 93 | bodyNode = undefined; 94 | headNode = undefined; 95 | } catch (e) { 96 | console.error(e); 97 | } 98 | } 99 | 100 | 101 | } 102 | 103 | 104 | @NgModule({ 105 | providers: [ 106 | { provide: TransferState, useClass: ServerTransferState } 107 | ] 108 | }) 109 | export class ServerTransferStateModule { 110 | 111 | } 112 | 113 | 114 | export { TransferState, TransferHttp, TransferHttpModule } from './transfer-state'; 115 | -------------------------------------------------------------------------------- /src/transfer-state.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgModule, Inject, Optional, Host } from '@angular/core'; 2 | import { 3 | ConnectionBackend, 4 | Http, 5 | Request, 6 | RequestOptions, 7 | RequestOptionsArgs, 8 | Response 9 | } from '@angular/http'; 10 | import { Observable } from 'rxjs/Observable'; 11 | 12 | import 'rxjs/add/operator/map'; 13 | import 'rxjs/add/operator/do'; 14 | import 'rxjs/add/observable/of'; 15 | 16 | function __transferStateKey(url: string, options: any): string { 17 | return url + JSON.stringify(options); 18 | } 19 | 20 | @Injectable() 21 | export class TransferState { 22 | static KEY = 'TransferState'; 23 | private _map = new Map(); 24 | 25 | constructor() {} 26 | 27 | keys() { 28 | return this._map.keys(); 29 | } 30 | 31 | get(key: string): any { 32 | return this._map.get(key); 33 | } 34 | 35 | set(key: string, value: any): Map { 36 | return this._map.set(key, value); 37 | } 38 | 39 | toJson(): any { 40 | const obj: any = {}; 41 | this._map.forEach((value: any, key: string, map: Map) => obj[key] = value); 42 | return obj; 43 | } 44 | 45 | initialize(obj: any): void { 46 | Object.keys(obj).forEach((key: string) => this.set(key, obj[key])); 47 | } 48 | 49 | inject(location?: string): void {} 50 | } 51 | 52 | type iCallback = (uri: string | Request, body: any, options?: RequestOptionsArgs) => Observable; 53 | 54 | @Injectable() 55 | export class TransferHttp { 56 | constructor(private http: Http, 57 | protected transferState: TransferState, 58 | @Optional() @Host() @Inject('TransferStateKey') private transferStateKey?: any) { 59 | if (!this.transferStateKey) { 60 | this.transferStateKey = __transferStateKey; 61 | } 62 | } 63 | 64 | request(uri: string | Request, options?: RequestOptionsArgs): Observable { 65 | return this.getData(uri, options, (url: string, options: RequestOptionsArgs) => { 66 | return this.http.request(url, options); 67 | }); 68 | } 69 | /** 70 | * Performs a request with `get` http method. 71 | */ 72 | get(url: string, options?: RequestOptionsArgs): Observable { 73 | return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { 74 | return this.http.get(url, options); 75 | }); 76 | } 77 | /** 78 | * Performs a request with `post` http method. 79 | */ 80 | post(url: string, body: any, options?: RequestOptionsArgs): Observable { 81 | return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { 82 | return this.http.post(url, body. options); 83 | }); 84 | } 85 | /** 86 | * Performs a request with `put` http method. 87 | */ 88 | put(url: string, body: any, options?: RequestOptionsArgs): Observable { 89 | return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { 90 | return this.http.put(url, options); 91 | }); 92 | } 93 | /** 94 | * Performs a request with `delete` http method. 95 | */ 96 | delete(url: string, options?: RequestOptionsArgs): Observable { 97 | return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { 98 | return this.http.delete(url, options); 99 | }); 100 | } 101 | /** 102 | * Performs a request with `patch` http method. 103 | */ 104 | patch(url: string, body: any, options?: RequestOptionsArgs): Observable { 105 | return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { 106 | return this.http.patch(url, body.options); 107 | }); 108 | } 109 | /** 110 | * Performs a request with `head` http method. 111 | */ 112 | head(url: string, options?: RequestOptionsArgs): Observable { 113 | return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { 114 | return this.http.head(url, options); 115 | }); 116 | } 117 | /** 118 | * Performs a request with `options` http method. 119 | */ 120 | options(url: string, options?: RequestOptionsArgs): Observable { 121 | return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { 122 | return this.http.options(url, options); 123 | }); 124 | } 125 | 126 | private getData(uri: string | Request, options: RequestOptionsArgs, callback: iCallback) { 127 | 128 | let url = uri; 129 | 130 | if (typeof uri !== 'string') { 131 | url = uri.url; 132 | } 133 | 134 | const key = this.transferStateKey(url, options); 135 | 136 | try { 137 | return this.resolveData(key); 138 | 139 | } catch (e) { 140 | return callback(uri, options) 141 | .map(res => res.json()) 142 | .do(data => { 143 | this.setCache(key, data); 144 | }); 145 | } 146 | } 147 | 148 | private getPostData(uri: string | Request, body: any, options: RequestOptionsArgs, callback: iCallback) { 149 | 150 | let url = uri; 151 | 152 | if (typeof uri !== 'string') { 153 | url = uri.url; 154 | } 155 | 156 | const key = this.transferStateKey(url, options); 157 | 158 | try { 159 | 160 | return this.resolveData(key); 161 | 162 | } catch (e) { 163 | return callback(uri, body, options) 164 | .map(res => res.json()) 165 | .do(data => { 166 | this.setCache(key, data); 167 | }); 168 | } 169 | } 170 | 171 | private resolveData(key: string) { 172 | const data = this.getFromCache(key); 173 | if (!data) { 174 | throw new Error(); 175 | } 176 | return Observable.of(data); 177 | } 178 | 179 | private setCache(key: string, data: any) { 180 | return this.transferState.set(key, data); 181 | } 182 | 183 | private getFromCache(key: string): any { 184 | return this.transferState.get(key); 185 | } 186 | } 187 | 188 | @NgModule({ 189 | providers: [ 190 | TransferHttp 191 | ] 192 | }) 193 | export class TransferHttpModule {} 194 | --------------------------------------------------------------------------------