├── .nvmrc ├── .bowerrc ├── modules ├── universal │ ├── client │ │ ├── index.ts │ │ ├── README.md │ │ ├── client.ts │ │ └── src │ │ │ ├── bootstrap.ts │ │ │ └── ng_preload_cache.ts │ ├── server │ │ ├── index.ts │ │ ├── README.md │ │ ├── src │ │ │ ├── server_patch.ts │ │ │ ├── directives │ │ │ │ └── server_form.ts │ │ │ ├── stringifyElement.ts │ │ │ ├── helper.ts │ │ │ ├── ng_preboot.ts │ │ │ ├── router │ │ │ │ └── server_router.ts │ │ │ ├── render │ │ │ │ └── server_dom_renderer.ts │ │ │ ├── render.ts │ │ │ ├── express │ │ │ │ └── engine.ts │ │ │ ├── ng_scripts.ts │ │ │ ├── platform │ │ │ │ └── node.ts │ │ │ └── http │ │ │ │ └── server_http.ts │ │ ├── server.ts │ │ └── test │ │ │ └── router_server_spec.ts │ ├── universal.d.ts │ ├── package.json │ ├── tsconfig.json │ └── README.md ├── preboot │ ├── client.ts │ ├── server.ts │ ├── src │ │ ├── server │ │ │ ├── preboot_server.ts │ │ │ ├── utils.ts │ │ │ ├── presets.ts │ │ │ ├── client_code_generator.ts │ │ │ └── normalize.ts │ │ ├── interfaces │ │ │ ├── event.ts │ │ │ ├── strategy.ts │ │ │ ├── element.ts │ │ │ ├── preboot_options.ts │ │ │ └── preboot_ref.ts │ │ └── client │ │ │ ├── listen │ │ │ ├── listen_by_attributes.ts │ │ │ ├── listen_by_selectors.ts │ │ │ └── listen_by_event_bindings.ts │ │ │ ├── replay │ │ │ ├── replay_after_hydrate.ts │ │ │ └── replay_after_rerender.ts │ │ │ ├── freeze │ │ │ └── freeze_with_spinner.ts │ │ │ ├── log.ts │ │ │ ├── buffer_manager.ts │ │ │ ├── preboot_client.ts │ │ │ ├── event_manager.ts │ │ │ └── dom.ts │ ├── tsconfig.json │ ├── test │ │ ├── preboot_karma.ts │ │ ├── client │ │ │ ├── log_spec.ts │ │ │ ├── listen │ │ │ │ ├── listen_by_selectors_spec.ts │ │ │ │ ├── listen_by_attributes_spec.ts │ │ │ │ └── listen_by_event_bindings_spec.ts │ │ │ ├── freeze │ │ │ │ └── freeze_with_spinner_spec.ts │ │ │ ├── replay │ │ │ │ ├── replay_after_hydrate_spec.ts │ │ │ │ └── replay_after_rerender_spec.ts │ │ │ ├── buffer_manager_spec.ts │ │ │ └── event_manager_spec.ts │ │ └── server │ │ │ ├── utils_spec.ts │ │ │ ├── client_code_generator_spec.ts │ │ │ ├── presets_spec.ts │ │ │ └── normalize_spec.ts │ ├── package.json │ └── README.md └── index.ts ├── .npmignore ├── examples ├── app │ ├── public │ │ └── css │ │ │ ├── bg.png │ │ │ └── main.css │ ├── universal │ │ ├── todo │ │ │ ├── css │ │ │ │ ├── bg.png │ │ │ │ └── main.css │ │ │ ├── index.ng2.html │ │ │ ├── services │ │ │ │ └── TodoStore.ts │ │ │ ├── todo.html │ │ │ └── app.ts │ │ └── test_page │ │ │ ├── index.ng2.html │ │ │ └── app.ts │ └── server │ │ ├── server.ts │ │ ├── api.ts │ │ └── routes.ts └── preboot │ ├── preboot.css │ └── preboot_example.html ├── custom_typings ├── _custom.d.ts ├── server.d.ts ├── gulp-uglify.d.ts ├── vinyl-buffer.d.ts ├── gulp-insert.d.ts └── event-stream.d.ts ├── .settings └── settings.json ├── .editorconfig ├── scripts └── update-ng-bundle.sh ├── bower.json ├── tsd.json ├── LICENSE ├── .gitignore ├── CHANGELOG.md ├── tslint.json ├── .travis.yml ├── index.js ├── protractor.conf.js ├── README.md ├── package.json └── tsconfig.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 0.12 2 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /modules/universal/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './client'; 2 | -------------------------------------------------------------------------------- /modules/universal/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from './server'; 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | angular 2 | node_modules 3 | web_modules 4 | dist 5 | -------------------------------------------------------------------------------- /modules/universal/client/README.md: -------------------------------------------------------------------------------- 1 | # Angular2 Server Rendering 2 | 3 | -------------------------------------------------------------------------------- /modules/universal/server/README.md: -------------------------------------------------------------------------------- 1 | # Angular2 Server Rendering 2 | 3 | -------------------------------------------------------------------------------- /modules/preboot/client.ts: -------------------------------------------------------------------------------- 1 | export * from './src/client/preboot_client'; 2 | -------------------------------------------------------------------------------- /modules/preboot/server.ts: -------------------------------------------------------------------------------- 1 | export * from './src/server/preboot_server'; 2 | -------------------------------------------------------------------------------- /examples/app/public/css/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playground/universal/master/examples/app/public/css/bg.png -------------------------------------------------------------------------------- /modules/universal/client/client.ts: -------------------------------------------------------------------------------- 1 | export * from './src/ng_preload_cache'; 2 | export * from './src/bootstrap'; 3 | -------------------------------------------------------------------------------- /modules/preboot/src/server/preboot_server.ts: -------------------------------------------------------------------------------- 1 | export {getClientCodeStream, getClientCode} from './client_code_generator'; 2 | -------------------------------------------------------------------------------- /examples/app/universal/todo/css/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playground/universal/master/examples/app/universal/todo/css/bg.png -------------------------------------------------------------------------------- /modules/index.ts: -------------------------------------------------------------------------------- 1 | // this is used as a hack for the TypeScript compiler so dist will have all modules 2 | export function doNothing() { 3 | return 0; 4 | } 5 | -------------------------------------------------------------------------------- /modules/universal/universal.d.ts: -------------------------------------------------------------------------------- 1 | export function ng2engineWithPreboot(): any; 2 | export function ng2engine(): any; 3 | export function bootstrap(component: any, providers: any): any; 4 | -------------------------------------------------------------------------------- /custom_typings/_custom.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | -------------------------------------------------------------------------------- /.settings/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.excludeFolders": [ 4 | ".git", 5 | "node_modules", 6 | "bower_components", 7 | "packages", 8 | "build", 9 | "dist", 10 | "tmp" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /custom_typings/server.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module "xhr2" { 3 | class XMLHttpRequest { 4 | nodejsSet(url: any): any; 5 | } 6 | export = XMLHttpRequest; 7 | } 8 | 9 | declare module "angular2_server" { 10 | function bootstrap(appComponentType: any, appInjector: any, componentInjectableBindings?: Array, errorReporter?: Function): any; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | 17 | [*.json] 18 | insert_final_newline = false 19 | -------------------------------------------------------------------------------- /scripts/update-ng-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "" 3 | echo "updating scripts from angular/dist/bundle" 4 | 5 | mkdir -p ./web_modules 6 | # angular submodule 7 | 8 | # cp -fR ./angular/dist/. ./web_modules/ 9 | 10 | # angular npm 11 | mkdir -p ./web_modules/js/bundle 12 | cp -fR ./node_modules/angular2/bundles/. ./web_modules/js/bundle 13 | 14 | echo "done!" 15 | echo "" 16 | -------------------------------------------------------------------------------- /custom_typings/gulp-uglify.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for vinyl-source-stream 2 | // Project: https://github.com/terinjokes/gulp-uglify 3 | // Definitions by: Jeff Whelpley 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module "gulp-uglify" { 7 | function uglify(): NodeJS.ReadWriteStream; 8 | export = uglify; 9 | } 10 | -------------------------------------------------------------------------------- /custom_typings/vinyl-buffer.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for vinyl-buffer 2 | // Project: https://github.com/hughsk/vinyl-buffer 3 | // Definitions by: Jeff Whelpley 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module "vinyl-buffer" { 7 | function vinylBuffer(): NodeJS.ReadWriteStream; 8 | export = vinylBuffer; 9 | } 10 | -------------------------------------------------------------------------------- /modules/preboot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "listFiles": false, 13 | "outDir": "dist" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /modules/universal/server/src/server_patch.ts: -------------------------------------------------------------------------------- 1 | // polyfills 2 | import 'es6-promise'; 3 | import 'es6-shim'; 4 | // typescript emit metadata 5 | import 'reflect-metadata'; 6 | // zone.js to track promises 7 | import 'zone.js/dist/zone-microtask'; 8 | import 'zone.js/dist/long-stack-trace-zone'; 9 | 10 | // dom closure 11 | import {Parse5DomAdapter} from 'angular2/src/platform/server/parse5_adapter'; 12 | Parse5DomAdapter.makeCurrent(); 13 | -------------------------------------------------------------------------------- /examples/app/universal/test_page/index.ng2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Universal Angular 2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /custom_typings/gulp-insert.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for vinyl-source-stream 2 | // Project: https://github.com/rschmukler/gulp-insert 3 | // Definitions by: Jeff Whelpley 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module "gulp-insert" { 7 | 8 | interface GulpInsert { 9 | append(str: String): NodeJS.ReadWriteStream; 10 | } 11 | 12 | var insert: GulpInsert; 13 | 14 | export = insert; 15 | } 16 | -------------------------------------------------------------------------------- /custom_typings/event-stream.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for vinyl-buffer 2 | // Project: https://github.com/dominictarr/event-stream 3 | // Definitions by: Jeff Whelpley 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | declare module "event-stream" { 7 | 8 | interface EventStream { 9 | map(cb: Function): NodeJS.ReadWriteStream; 10 | } 11 | 12 | var eventStream: EventStream; 13 | 14 | export = eventStream; 15 | } 16 | -------------------------------------------------------------------------------- /examples/app/public/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | line-height: 1.4em; 10 | background: #eaeaea; 11 | color: #4d4d4d; 12 | width: 550px; 13 | margin: 0 auto; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-font-smoothing: antialiased; 16 | -ms-font-smoothing: antialiased; 17 | -o-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | } 20 | -------------------------------------------------------------------------------- /examples/app/universal/todo/css/main.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | body { 8 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 9 | line-height: 1.4em; 10 | background: #eaeaea; 11 | color: #4d4d4d; 12 | width: 550px; 13 | margin: 0 auto; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-font-smoothing: antialiased; 16 | -ms-font-smoothing: antialiased; 17 | -o-font-smoothing: antialiased; 18 | font-smoothing: antialiased; 19 | } 20 | -------------------------------------------------------------------------------- /modules/preboot/src/interfaces/event.ts: -------------------------------------------------------------------------------- 1 | // combo of a node and eventName used by listeners 2 | export interface NodeEvent { 3 | node: any; 4 | eventName: string; 5 | } 6 | 7 | // our wrapper around DOM events in preboot 8 | export interface PrebootEvent { 9 | node: any; 10 | event: any; 11 | name: string; 12 | time?: number; 13 | } 14 | 15 | // an actual DOM event object 16 | export interface DomEvent { 17 | which?: number; 18 | type?: string; 19 | target?: any; 20 | preventDefault(); 21 | } 22 | -------------------------------------------------------------------------------- /modules/preboot/test/preboot_karma.ts: -------------------------------------------------------------------------------- 1 | 2 | // this is the entry point for karma tests 3 | import './client/freeze/freeze_with_spinner_spec'; 4 | import './client/listen/listen_by_attributes_spec'; 5 | import './client/listen/listen_by_event_bindings_spec'; 6 | import './client/listen/listen_by_selectors_spec'; 7 | import './client/replay/replay_after_hydrate_spec'; 8 | import './client/replay/replay_after_rerender_spec'; 9 | import './client/buffer_manager_spec'; 10 | import './client/dom_spec'; 11 | import './client/event_manager_spec'; 12 | import './client/log_spec'; 13 | -------------------------------------------------------------------------------- /modules/universal/server/server.ts: -------------------------------------------------------------------------------- 1 | import './src/server_patch'; 2 | 3 | export * from './src/directives/server_form'; 4 | 5 | export * from './src/express/engine'; 6 | 7 | export * from './src/http/server_http'; 8 | 9 | export * from './src/platform/node'; 10 | 11 | export * from './src/render/server_dom_renderer'; 12 | 13 | export * from './src/router/server_router'; 14 | 15 | export * from './src/helper'; 16 | export * from './src/ng_preboot'; 17 | export * from './src/ng_scripts'; 18 | export * from './src/render'; 19 | export * from './src/stringifyElement'; 20 | -------------------------------------------------------------------------------- /modules/preboot/src/interfaces/strategy.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from './preboot_ref'; 2 | import {Element} from './element'; 3 | import {DomEvent} from './event'; 4 | 5 | export interface ListenStrategy { 6 | name?: string; 7 | attributeName?: string; 8 | eventsBySelector?: Object; 9 | preventDefault?: boolean; 10 | trackFocus?: boolean; 11 | doNotReplay?: boolean; 12 | dispatchEvent?: string; 13 | action?(preboot: PrebootRef, node: Element, event: DomEvent); 14 | } 15 | 16 | export interface ReplayStrategy { 17 | name?: string; 18 | checkIfExists?: boolean; 19 | } 20 | -------------------------------------------------------------------------------- /examples/app/universal/todo/index.ng2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Todo Angular 2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | Loading... 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-universal", 3 | "main": "index.js", 4 | "version": "0.0.1", 5 | "description": "Universal (isomorphic) javascript support for Angular2", 6 | "homepage": "https://github.com/angular/universal", 7 | "license": "Apache-2.0", 8 | "authors": [ 9 | "Tobias Bosch ", 10 | "PatrickJS ", 11 | "Jeff Whelpley " 12 | ], 13 | "ignore": [ 14 | "**/.*", 15 | "node_modules", 16 | "bower_components", 17 | "web_components", 18 | "test", 19 | "tests" 20 | ], 21 | "dependencies": { 22 | "reflect-metadata": "~0.1.0", 23 | "traceur-runtime": "~0.0.90", 24 | "system.js": "~0.18.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /modules/universal/client/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {Type} from 'angular2/src/facade/lang'; 2 | import {Provider} from 'angular2/core'; 3 | import {bootstrap as bootstrapClient} from 'angular2/bootstrap'; 4 | import {ComponentRef} from 'angular2/src/core/linker/dynamic_component_loader'; 5 | import {Promise} from 'angular2/src/facade/async'; 6 | 7 | export function bootstrap(appComponentType: /*Type*/ any, 8 | appProviders: Array = null): 9 | Promise { 10 | 11 | return bootstrapClient(appComponentType, appProviders) 12 | .then((appRef: ComponentRef) => { 13 | if ('preboot' in window) { 14 | (window).preboot.complete(); 15 | } 16 | return appRef; 17 | }); 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /modules/preboot/test/client/log_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {log} from '../../src/client/log'; 4 | 5 | describe('log', function () { 6 | describe('log()', function () { 7 | it('chould call replaySuccess w appropriate console.logs', function () { 8 | let consoleLog = console.log; 9 | spyOn(console, 'log'); 10 | 11 | let serverNode = { name: 'serverNode' }; 12 | let clientNode = { name: 'clientNode' }; 13 | let evt = { name: 'evt1' }; 14 | 15 | log(3, serverNode, clientNode, evt); 16 | 17 | expect(console.log).toHaveBeenCalledWith('replaying:'); 18 | expect(console.log).toHaveBeenCalledWith({ 19 | serverNode: serverNode, 20 | clientNode: clientNode, 21 | event: evt 22 | }); 23 | 24 | console.log = consoleLog; 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /modules/preboot/src/interfaces/element.ts: -------------------------------------------------------------------------------- 1 | import {DomEvent} from './event'; 2 | 3 | export interface Style { 4 | display: string; 5 | } 6 | 7 | export interface Element { 8 | id?: string; 9 | value?: string; 10 | checked?: boolean; 11 | selected?: boolean; 12 | tagName?: string; 13 | nodeName?: string; 14 | className?: string; 15 | selectionStart?: number; 16 | selectionEnd?: number; 17 | selectionDirection?: string; 18 | selection?: any; 19 | createTextRange?(): any; 20 | setSelectionRange?(fromPos: number, toPos: number, direction: string); 21 | style?: Style; 22 | parentNode?: Element; 23 | childNodes?: Element[]; 24 | attributes?: string[]; 25 | remove?(); 26 | focus?(); 27 | dispatchEvent?(event: DomEvent); 28 | getAttribute?(name: string): string; 29 | cloneNode?(deep: boolean): Element; 30 | insertBefore?(nodeToInsert: Element, beforeNode: Element); 31 | } 32 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "borisyankov/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "tsd_typings", 6 | "bundle": "tsd_typings/tsd.d.ts", 7 | "installed": { 8 | "node/node.d.ts": { 9 | "commit": "001ca36ba58cef903c4c063555afb07bbc36bb58" 10 | }, 11 | "q/Q.d.ts": { 12 | "commit": "c7b1128cc9a8f5797bade826e7632b36b06a856c" 13 | }, 14 | "gulp-rename/gulp-rename.d.ts": { 15 | "commit": "c7b1128cc9a8f5797bade826e7632b36b06a856c" 16 | }, 17 | "vinyl-source-stream/vinyl-source-stream.d.ts": { 18 | "commit": "c7b1128cc9a8f5797bade826e7632b36b06a856c" 19 | }, 20 | "lodash/lodash.d.ts": { 21 | "commit": "ab3cc82066805007cbf0db233ddb5879f42be4d2" 22 | }, 23 | "browserify/browserify.d.ts": { 24 | "commit": "6baa15a0d760170b4aeefab68d581a1ea4034aa6" 25 | }, 26 | "gulp/gulp.d.ts": { 27 | "commit": "6baa15a0d760170b4aeefab68d581a1ea4034aa6" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /modules/universal/server/src/directives/server_form.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, 3 | ElementRef, 4 | OpaqueToken, 5 | Injectable, 6 | Optional, 7 | Inject 8 | } from 'angular2/core'; 9 | 10 | import {Renderer} from 'angular2/core'; 11 | import {isPresent, CONST_EXPR} from 'angular2/src/facade/lang'; 12 | 13 | export const APP_LOCATION: OpaqueToken = CONST_EXPR(new OpaqueToken('appLocation')); 14 | 15 | @Directive({ 16 | selector: 'form', 17 | host: { 18 | 'method': 'POST' 19 | } 20 | }) 21 | export class ServerForm { 22 | constructor( 23 | element: ElementRef, 24 | renderer: Renderer, 25 | @Optional() @Inject(APP_LOCATION) appLocation?: string) { 26 | 27 | let url: string = '/'; 28 | if (typeof window === 'object' && 'location' in window) { 29 | // Grab Browser location if browser 30 | url = window.location.toString(); 31 | } 32 | appLocation = isPresent(appLocation) ? appLocation : url; 33 | 34 | 35 | renderer.setElementAttribute(element, 'action', appLocation); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /modules/preboot/test/server/utils_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {stringifyWithFunctions} from '../../src/server/utils'; 4 | /* tslint:disable:eofline no-trailing-whitespace */ 5 | 6 | /** 7 | * No downstream deps for utils, so easy to test 8 | */ 9 | describe('utils', function () { 10 | describe('stringifyWithFunctions', function () { 11 | it('should do the same thing as stringify if no functions', function () { 12 | let obj = { foo: 'choo', woo: 'loo', zoo: 5 }; 13 | let expected = JSON.stringify(obj); 14 | let actual = stringifyWithFunctions(obj); 15 | expect(actual).toEqual(expected); 16 | }); 17 | 18 | it('should stringify an object with functions', function () { 19 | let obj = { blah: 'foo', zoo: function (blah) { 20 | return blah + 1; 21 | }}; 22 | let expected = '{"blah":"foo","zoo":function ('; 23 | let actual = stringifyWithFunctions(obj); 24 | expect(actual.substring(0, 30)).toEqual(expected); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/app/server/server.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | var express = require('express'); 4 | var serveStatic = require('serve-static'); 5 | var morgan = require('morgan'); 6 | var path = require('path'); 7 | 8 | module.exports = function(ROOT) { 9 | var app = express(); 10 | var universal = require(`${ROOT}/dist/modules/universal/server`); 11 | // rendering engine 12 | 13 | app.engine('ng2.html', universal.ng2engine); 14 | app.set('views', path.join(ROOT, 'examples')); 15 | app.set('view engine', 'ng2.html'); 16 | app.set('view options', { doctype: 'html' }); 17 | 18 | var routes = require('./routes'); 19 | var api = require('./api'); 20 | 21 | 22 | app.use(serveStatic(`${ROOT}/dist`)); 23 | app.use(serveStatic(`${ROOT}/examples/app/public`)); 24 | 25 | app.use('/api', api(ROOT)); 26 | app.use(routes(ROOT)); 27 | 28 | 29 | app.use(morgan('dev')); 30 | 31 | app.get('*', function(req, res) { 32 | res.json({ 33 | 'route': 'Sorry this page does not exist!' 34 | }); 35 | }); 36 | 37 | return app; 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /modules/preboot/src/client/listen/listen_by_attributes.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {ListenStrategy} from '../../interfaces/strategy'; 3 | import {NodeEvent} from '../../interfaces/event'; 4 | 5 | /** 6 | * This listen strategy will look for a specific attribute which contains all the elements 7 | * that a given element is listening to. For ex.
8 | */ 9 | export function getNodeEvents(preboot: PrebootRef, strategy: ListenStrategy): NodeEvent[] { 10 | let attributeName = strategy.attributeName || 'preboot-events'; 11 | let elems = preboot.dom.getAllAppNodes('[' + attributeName + ']'); 12 | 13 | // if no elements found, return empty array since no node events 14 | if (!elems) { return []; } 15 | 16 | // else loop through all the elems and add node events 17 | let nodeEvents = []; 18 | for (let elem of elems) { 19 | let events = elem.getAttribute(attributeName).split(','); 20 | 21 | for (let eventName of events) { 22 | nodeEvents.push({ 23 | node: elem, 24 | eventName: eventName 25 | }); 26 | } 27 | } 28 | 29 | return nodeEvents; 30 | } 31 | -------------------------------------------------------------------------------- /modules/preboot/src/client/replay/replay_after_hydrate.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {ReplayStrategy} from '../../interfaces/strategy'; 3 | import {PrebootEvent} from '../../interfaces/event'; 4 | 5 | /** 6 | * this replay strategy assumes that the client did not blow away 7 | * the server generated HTML and that the elements in memory for 8 | * preboot can be used to replay the events. 9 | * 10 | * any events that could not be replayed for whatever reason are returned. 11 | */ 12 | export function replayEvents(preboot: PrebootRef, strategy: ReplayStrategy, events: PrebootEvent[]): PrebootEvent[] { 13 | let remainingEvents = []; 14 | events = events || []; 15 | 16 | for (let eventData of events) { 17 | let event = eventData.event; 18 | let node = eventData.node; 19 | 20 | // if we should check to see if the node exists in the DOM before dispatching 21 | // note: this can be expensive so this option is false by default 22 | if (strategy.checkIfExists && !preboot.dom.appContains(node)) { 23 | remainingEvents.push(eventData); 24 | } else { 25 | node.dispatchEvent(event); 26 | } 27 | } 28 | 29 | return remainingEvents; 30 | } 31 | -------------------------------------------------------------------------------- /modules/preboot/src/server/utils.ts: -------------------------------------------------------------------------------- 1 | const FUNC_START = 'START_FUNCTION_HERE'; 2 | const FUNC_STOP = 'STOP_FUNCTION_HERE'; 3 | 4 | /** 5 | * Stringify an object and include functions 6 | */ 7 | export function stringifyWithFunctions(obj: Object): String { 8 | 9 | // first stringify except mark off functions with markers 10 | let str = JSON.stringify(obj, function (key, value) { 11 | 12 | // if the value is a function, we want to wrap it with markers 13 | if (!!(value && value.constructor && value.call && value.apply)) { 14 | return FUNC_START + value.toString() + FUNC_STOP; 15 | } else { 16 | return value; 17 | } 18 | }); 19 | 20 | // now we use the markers to replace function strings with actual functions 21 | let startFuncIdx = str.indexOf(FUNC_START); 22 | let stopFuncIdx, fn; 23 | while (startFuncIdx >= 0) { 24 | stopFuncIdx = str.indexOf(FUNC_STOP); 25 | 26 | // pull string out 27 | fn = str.substring(startFuncIdx + FUNC_START.length, stopFuncIdx); 28 | fn = fn.replace(/\\n/g, '\n'); 29 | 30 | str = str.substring(0, startFuncIdx - 1) + fn + str.substring(stopFuncIdx + FUNC_STOP.length + 1); 31 | startFuncIdx = str.indexOf(FUNC_START); 32 | } 33 | 34 | return str; 35 | } 36 | -------------------------------------------------------------------------------- /modules/preboot/src/client/listen/listen_by_selectors.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {ListenStrategy} from '../../interfaces/strategy'; 3 | import {NodeEvent} from '../../interfaces/event'; 4 | 5 | /** 6 | * This listen strategy uses a list of selectors maped to events. For example: 7 | * { 8 | * 'input[type="text"],textarea': ['focusin', 'focusout'], 9 | * 'button': ['click'] 10 | * } 11 | */ 12 | export function getNodeEvents(preboot: PrebootRef, strategy: ListenStrategy): NodeEvent[] { 13 | let nodeEvents = []; 14 | let eventsBySelector = strategy.eventsBySelector || {}; 15 | let selectors = Object.keys(eventsBySelector); 16 | 17 | // loop through selectors 18 | for (let selector of selectors) { 19 | let events = eventsBySelector[selector]; 20 | let elems = preboot.dom.getAllAppNodes(selector); 21 | 22 | // if no elems, go to next iteration in for loop 23 | if (!elems) { continue; } 24 | 25 | // for each node and eventName combo, add a nodeEvent 26 | for (let elem of elems) { 27 | for (let eventName of events) { 28 | nodeEvents.push({ 29 | node: elem, 30 | eventName: eventName 31 | }); 32 | } 33 | } 34 | } 35 | 36 | return nodeEvents; 37 | } 38 | -------------------------------------------------------------------------------- /modules/preboot/src/interfaces/preboot_options.ts: -------------------------------------------------------------------------------- 1 | 2 | interface Styles { 3 | className?: string; 4 | style?: Object; 5 | } 6 | 7 | interface FreezeStyles { 8 | overlay?: Styles; 9 | spinner?: Styles; 10 | } 11 | 12 | // interface simply for type checking options values passed into preboot 13 | export interface PrebootOptions { 14 | listen?: any; // can be string (name of strategy), object (custom) or array of either 15 | replay?: any; // same as listen 16 | freeze?: any; // same as listen 17 | appRoot?: string; // a selector for the root of the application 18 | pauseEvent?: string; // name of event that when dispatched on document will cause preboot to pause 19 | resumeEvent?: string; // when this event dispatched on document, preboot will resume 20 | completeEvent?: string; // instead of calling complete(), can just raise this event 21 | presets?: any; // each string represents a preset value 22 | uglify?: boolean; // if true, client code generated will be uglified 23 | buffer?: boolean; // if true, attempt to buffer client rendering to hidden div 24 | debug?: boolean; // if true, output console logs on the client with runtime info about preboot 25 | } 26 | -------------------------------------------------------------------------------- /modules/preboot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preboot", 3 | "version": "1.0.2", 4 | "description": "Record and play back client side events", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/angular/universal" 12 | }, 13 | "author": "Jeff Whelpley", 14 | "license": "Apache-2.0", 15 | "contributors": [ 16 | "Tobias Bosch ", 17 | "PatrickJS ", 18 | "Jeff Whelpley " 19 | ], 20 | "bugs": { 21 | "url": "https://github.com/angular/universal/issues" 22 | }, 23 | "homepage": "https://github.com/angular/universal/tree/master/modules/preboot", 24 | "devDependencies": { 25 | "gulp-jasmine": "^2.1.0", 26 | "jasmine": "^2.3.2", 27 | "jasmine-reporters": "^2.0.7", 28 | "jasmine-spec-reporter": "^2.4.0", 29 | "karma": "^0.13.11", 30 | "karma-jasmine": "^0.3.6" 31 | }, 32 | "dependencies": { 33 | "browserify": "^11.2.0", 34 | "event-stream": "^3.3.2", 35 | "gulp": "^3.9.0", 36 | "gulp-insert": "^0.5.0", 37 | "gulp-rename": "^1.2.2", 38 | "gulp-uglify": "^1.4.2", 39 | "lodash": "^3.10.1", 40 | "q": "^1.4.1", 41 | "vinyl-buffer": "^1.0.0", 42 | "vinyl-source-stream": "^1.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | .idea 5 | .vscode 6 | .DS_Store 7 | **/.DS_Store 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | /lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | /coverage 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # Dependency directory 24 | # Commenting this out is preferred by some people, see 25 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 26 | node_modules 27 | modules/preboot/node_modules 28 | 29 | # used for karma unit test coverage 30 | test/coverage 31 | 32 | # Users Environment Variables 33 | .lock-wscript 34 | 35 | # Server 36 | /dist/ 37 | /modules/server/dist/ 38 | # /modules/server/typings/ 39 | !/modules/server/typings/_custom 40 | !/modules/server/typings/tsd.d.ts 41 | 42 | # Static files 43 | 44 | /bower_components/ 45 | /web_modules/ 46 | 47 | # Preboot 48 | /modules/preboot/node_modules 49 | /modules/preboot/dist 50 | # /modules/preboot/typings/ 51 | !/modules/preboot/typings/_custom 52 | !/modules/preboot/typings/tsd.d.ts 53 | 54 | # inline ts compile 55 | modules/**/*.js 56 | 57 | # Typings 58 | /typings 59 | /modules/universal/client/typings 60 | /modules/universal/server/typings 61 | /tsd_typings 62 | 63 | /modules/universal/dist/ 64 | -------------------------------------------------------------------------------- /modules/preboot/test/client/listen/listen_by_selectors_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {getNodeEvents} from '../../../src/client/listen/listen_by_selectors'; 4 | 5 | describe('listen_by_selectors', function () { 6 | describe('getNodeEvents()', function () { 7 | it('should return nothing if nothing from query', function () { 8 | let preboot = { 9 | dom: { 10 | getAllAppNodes: () => null 11 | } 12 | }; 13 | let strategy = { 14 | eventsBySelector: { 'div.blah': ['evt1', 'evt2'] } 15 | }; 16 | let expected = []; 17 | let actual = getNodeEvents(preboot, strategy); 18 | expect(actual).toEqual(expected); 19 | }); 20 | 21 | it('should return node events', function () { 22 | let preboot = { 23 | dom: { 24 | getAllAppNodes: () => [{ name: 'one' }, { name: 'two' }] 25 | } 26 | }; 27 | let strategy = { 28 | eventsBySelector: { 'div.blah': ['evt1', 'evt2'] } 29 | }; 30 | let expected = [ 31 | { node: { name: 'one' }, eventName: 'evt1' }, 32 | { node: { name: 'one' }, eventName: 'evt2' }, 33 | { node: { name: 'two' }, eventName: 'evt1' }, 34 | { node: { name: 'two' }, eventName: 'evt2' } 35 | ]; 36 | let actual = getNodeEvents(preboot, strategy); 37 | expect(actual).toEqual(expected); 38 | }); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /modules/universal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-universal-preview", 3 | "main": "dist/server/server.js", 4 | "browser": "dist/client/client.js", 5 | "typings": "universal.d.ts", 6 | "typescript": { "definition": "universal.d.ts" }, 7 | "version": "0.24.0", 8 | "description": "Universal (isomorphic) javascript support for Angular2", 9 | "homepage": "https://github.com/angular/universal", 10 | "license": "Apache-2.0", 11 | "contributors": [ 12 | "Tobias Bosch ", 13 | "PatrickJS ", 14 | "Jeff Whelpley " 15 | ], 16 | "scripts": { 17 | "build": "tsc || true", 18 | "prepublish": "npm run build" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/angular/universal" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/angular/universal/issues" 26 | }, 27 | "devDependencies": { 28 | "typescript": "^1.7.3" 29 | }, 30 | "dependencies": { 31 | "css": "^2.2.1", 32 | "es6-shim": "^0.33.6", 33 | "parse5": "^1.5.0", 34 | "preboot": "^1.0.2", 35 | "angular2": "2.0.0-beta.0", 36 | "es6-promise": "^3.0.2", 37 | "reflect-metadata": "0.1.2", 38 | "rxjs": "5.0.0-beta.0", 39 | "zone.js": "0.5.10", 40 | "xhr2": "0.1.3" 41 | }, 42 | "peerDependencies": { 43 | "rxjs": "5.0.0-beta.0", 44 | "angular2": "2.0.0-beta.0", 45 | "css": "*", 46 | "parse5": "^1.5.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /modules/preboot/test/client/listen/listen_by_attributes_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {getNodeEvents} from '../../../src/client/listen/listen_by_attributes'; 4 | 5 | describe('listen_by_attributes', function () { 6 | describe('getNodeEvents()', function () { 7 | 8 | it('should return nothing if no selection found', function () { 9 | let preboot = { 10 | dom: { 11 | getAllAppNodes: function () { return null; } 12 | } 13 | }; 14 | let strategy = {}; 15 | let expected = []; 16 | let actual = getNodeEvents(preboot, strategy); 17 | expect(actual).toEqual(expected); 18 | }); 19 | 20 | it('should return node events for elems with attribute', function () { 21 | let nodes = [ 22 | { name: 'one', getAttribute: function () { return 'yo,mo'; }}, 23 | { name: 'two', getAttribute: function () { return 'shoo,foo'; }} 24 | ]; 25 | let preboot = { 26 | dom: { 27 | getAllAppNodes: function () { return nodes; } 28 | } 29 | }; 30 | let strategy = {}; 31 | let expected = [ 32 | { node: nodes[0], eventName: 'yo' }, 33 | { node: nodes[0], eventName: 'mo' }, 34 | { node: nodes[1], eventName: 'shoo' }, 35 | { node: nodes[1], eventName: 'foo' } 36 | ]; 37 | let actual = getNodeEvents(preboot, strategy); 38 | expect(actual).toEqual(expected); 39 | }); 40 | 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## 0.0.1 (2015-08-04) 3 | 4 | 5 | ### Bug Fixes 6 | 7 | * **bootstrap-server:** angular api changes ([01e9991](https://github.com/angular/universal/commit/01e9991)) 8 | * **http_server:** Http api changes ([48619de](https://github.com/angular/universal/commit/48619de)) 9 | * **karma.conf.js:** remove src bundles as these are not required ([78cb6f0](https://github.com/angular/universal/commit/78cb6f0)) 10 | * **minor:** Fixes for getClientCode and normalize ([20c4d9a](https://github.com/angular/universal/commit/20c4d9a)) 11 | * **todo.e2e.js:** revert from (keyup.enter) to (keyup) ([963b2c8](https://github.com/angular/universal/commit/963b2c8)) 12 | 13 | ### Features 14 | 15 | * **examples:** api todos ([c9b9c78](https://github.com/angular/universal/commit/c9b9c78)) 16 | * **examples:** include Http for app ([4b282f8](https://github.com/angular/universal/commit/4b282f8)) 17 | * **ng2Engine:** include different app state configs ([b4f43e7](https://github.com/angular/universal/commit/b4f43e7)) 18 | * **ng2Engine:** include more feature toggling ([3df2b7e](https://github.com/angular/universal/commit/3df2b7e)) 19 | * **preboot:** top level api ([de37473](https://github.com/angular/universal/commit/de37473)) 20 | * **protractor.conf.js:** delete cookies ([a8f131e](https://github.com/angular/universal/commit/a8f131e)) 21 | * **render:** inject preboot ([e131005](https://github.com/angular/universal/commit/e131005)) 22 | * **server:** allow bootstrap control ([641713f](https://github.com/angular/universal/commit/641713f)) 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /modules/preboot/src/client/replay/replay_after_rerender.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {ReplayStrategy} from '../../interfaces/strategy'; 3 | import {PrebootEvent} from '../../interfaces/event'; 4 | 5 | /** 6 | * This replay strategy assumes that the client completely re-rendered 7 | * the page so reboot will need to find the element in the new client 8 | * rendered DOM that matches the element it has in memory. 9 | * 10 | * Any events that could not be replayed for whatever reason are returned. 11 | */ 12 | export function replayEvents(preboot: PrebootRef, strategy: ReplayStrategy, events: PrebootEvent[]): PrebootEvent[] { 13 | let remainingEvents = []; 14 | events = events || []; 15 | 16 | // loop through the events, find the appropriate client node and dispatch the event 17 | for (let eventData of events) { 18 | let event = eventData.event; 19 | let serverNode = eventData.node; 20 | let clientNode = preboot.dom.findClientNode(serverNode); 21 | 22 | // if client node found, need to explicitly set value and then dispatch event 23 | if (clientNode) { 24 | clientNode.checked = serverNode.checked ? true : undefined; 25 | clientNode.selected = serverNode.selected ? true : undefined; 26 | clientNode.value = serverNode.value; 27 | clientNode.dispatchEvent(event); 28 | preboot.log(3, serverNode, clientNode, event); 29 | } else { 30 | remainingEvents.push(eventData); 31 | preboot.log(4, serverNode); 32 | } 33 | } 34 | 35 | return remainingEvents; 36 | } 37 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, "check-space"], 5 | "curly": true, 6 | "eofline": true, 7 | "forin": true, 8 | "indent": [true, "spaces"], 9 | "label-position": true, 10 | "label-undefined": true, 11 | "max-line-length": [true, 140], 12 | "no-arg": true, 13 | "no-bitwise": false, 14 | "no-console": [true, 15 | "debug", 16 | "info", 17 | "trace" 18 | ], 19 | "no-construct": true, 20 | "no-debugger": true, 21 | "no-duplicate-key": true, 22 | "no-duplicate-variable": true, 23 | "no-empty": false, 24 | "no-eval": true, 25 | "no-string-literal": true, 26 | "no-switch-case-fall-through": true, 27 | "no-trailing-comma": true, 28 | "no-trailing-whitespace": false, 29 | "no-unused-expression": true, 30 | "no-unused-variable": false, 31 | "no-unreachable": true, 32 | "no-use-before-declare": true, 33 | "no-var-keyword": false, 34 | "one-line": [true, 35 | "check-open-brace", 36 | "check-catch", 37 | "check-else", 38 | "check-whitespace" 39 | ], 40 | "quotemark": [true, "single"], 41 | "radix": true, 42 | "semicolon": true, 43 | "sort-object-literal-keys": false, 44 | "triple-equals": [true, "allow-null-check"], 45 | "variable-name": false, 46 | "whitespace": [true, 47 | "check-branch", 48 | "check-decl", 49 | "check-operator", 50 | "check-separator", 51 | "check-type" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/preboot/test/client/freeze/freeze_with_spinner_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {state, prep, cleanup} from '../../../src/client/freeze/freeze_with_spinner'; 4 | 5 | describe('freeze_with_spinner', function () { 6 | describe('cleanup()', function () { 7 | it('should call removeNode and null out overlay and spinner', function () { 8 | let preboot = { dom: { removeNode: null } }; 9 | 10 | state.overlay = 'boo'; 11 | state.spinner = 'food'; 12 | spyOn(preboot.dom, 'removeNode'); 13 | 14 | cleanup(preboot); 15 | 16 | expect(preboot.dom.removeNode).toHaveBeenCalledWith('boo'); 17 | expect(preboot.dom.removeNode).toHaveBeenCalledWith('food'); 18 | expect(state.overlay).toBeNull(); 19 | expect(state.spinner).toBeNull(); 20 | }); 21 | }); 22 | 23 | describe('prep()', function () { 24 | it('should call preboot fns trying to freeze UI', function () { 25 | let preboot = { 26 | dom: { 27 | addNodeToBody: function () { return { style: {} }; }, 28 | on: function () {}, 29 | removeNode: function () {} 30 | } 31 | }; 32 | let opts = {}; 33 | 34 | spyOn(preboot.dom, 'addNodeToBody'); 35 | spyOn(preboot.dom, 'on'); 36 | spyOn(preboot.dom, 'removeNode'); 37 | 38 | prep(preboot, opts); 39 | 40 | expect(preboot.dom.addNodeToBody).toHaveBeenCalled(); 41 | expect(preboot.dom.on).toHaveBeenCalled(); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /modules/preboot/src/interfaces/preboot_ref.ts: -------------------------------------------------------------------------------- 1 | import {PrebootOptions} from './preboot_options'; 2 | import {Element} from './element'; 3 | 4 | interface DomState { 5 | window?: Element; 6 | document?: Element; 7 | body?: Element; 8 | appRoot?: Element; 9 | serverRoot?: Element; 10 | clientRoot?: Element; 11 | } 12 | 13 | export interface CursorSelection { 14 | start?: number; 15 | end?: number; 16 | direction?: string; 17 | } 18 | 19 | // interface for the dom wrapper 20 | interface Dom { 21 | state?: DomState; 22 | init?(opts: any); 23 | updateRoots?(appRoot: Element, serverRoot?: Element, clientRoot?: Element); 24 | getDocumentNode?(selector: string): Element; 25 | getAppNode?(selector: string): Element; 26 | getAllAppNodes?(selector: string): Element[]; 27 | getClientNodes?(selector: string): Element[]; 28 | onLoad?(handler: Function); 29 | on?(eventName: string, handler: Function); 30 | dispatchGlobalEvent?(eventName: string); 31 | dispatchNodeEvent?(node: Element, eventName: string); 32 | appContains?(node: Element): Boolean; 33 | addNodeToBody?(type: string, className: string, styles: Object); 34 | removeNode?(node: Element); 35 | findClientNode?(serverNode: Element): Element; 36 | getSelection?(node: Element): CursorSelection; 37 | setSelection?(node: Element, selection: CursorSelection); 38 | } 39 | 40 | // interface for preboot modules available to strategies 41 | export interface PrebootRef { 42 | dom: Dom; 43 | log?: Function; 44 | activeNode?: any; 45 | time?: number; 46 | selection?: CursorSelection; 47 | } 48 | -------------------------------------------------------------------------------- /modules/preboot/test/server/client_code_generator_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as browserify from 'browserify'; 4 | import {ignoreUnusedStrategies, getClientCode} from '../../src/server/client_code_generator'; 5 | 6 | describe('clientCodeGenerator', function () { 7 | 8 | describe('ignoreUnusedStrategies()', function () { 9 | it('should filter out inactive strategies', function () { 10 | let b = browserify(); 11 | let bOpts = { something: 'yo' }; 12 | let strategyOpts = [{ name: 'foo' }, { name: 'choo' }]; 13 | let allStrategies = { foo: true, choo: true, zoo: true, moo: true }; 14 | let pathPrefix = 'prefix'; 15 | 16 | spyOn(b, 'ignore'); 17 | 18 | ignoreUnusedStrategies(b, bOpts, strategyOpts, allStrategies, pathPrefix); 19 | 20 | expect(b.ignore).not.toHaveBeenCalledWith(pathPrefix + 'foo.js', bOpts); 21 | expect(b.ignore).not.toHaveBeenCalledWith(pathPrefix + 'choo.js', bOpts); 22 | expect(b.ignore).toHaveBeenCalledWith(pathPrefix + 'zoo.js', bOpts); 23 | expect(b.ignore).toHaveBeenCalledWith(pathPrefix + 'moo.js', bOpts); 24 | }); 25 | }); 26 | 27 | describe('getClientCode()', function () { 28 | it('should get client code with a listen strategy', function (done) { 29 | let opts = { listen: [{ name: 'selectors' }], replay: [] }; 30 | getClientCode(opts, function (err, clientCode) { 31 | expect(err).toBeNull(); 32 | expect(clientCode).toMatch(/function getNodeEvents/); 33 | done(); 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /examples/app/universal/todo/services/TodoStore.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {Injectable} from 'angular2/core'; 3 | import {ListWrapper} from 'angular2/src/facade/collection'; 4 | // base model for RecordStore 5 | export class KeyModel { 6 | constructor(public key: number) {} 7 | } 8 | 9 | export class Todo extends KeyModel { 10 | constructor(key: number, public title: string, public completed: boolean) { super(key); } 11 | } 12 | 13 | @Injectable() 14 | export class TodoFactory { 15 | _uid: number = 0; 16 | 17 | nextUid(): number { return ++this._uid; } 18 | 19 | create(title: string, isCompleted: boolean): Todo { 20 | return new Todo(this.nextUid(), title, isCompleted); 21 | } 22 | } 23 | 24 | // store manages any generic item that inherits from KeyModel 25 | @Injectable() 26 | export class Store { 27 | list: Array = []; 28 | 29 | constructor() { 30 | 31 | } 32 | 33 | add(record: KeyModel): void { this.list.push(record); } 34 | 35 | remove(record: KeyModel): void { this._spliceOut(record); } 36 | 37 | removeBy(callback: any): void { 38 | var records = this.list.filter(callback); 39 | 40 | for (let i = 0; i < records.length; ++i) { 41 | let index = this.list.indexOf(records[i]); 42 | this.list.splice(index, 1); 43 | } 44 | } 45 | 46 | private _spliceOut(record: KeyModel) { 47 | var i = this._indexFor(record); 48 | if (i > -1) { 49 | return this.list.splice(i, 1)[0]; 50 | } 51 | return null; 52 | } 53 | 54 | private _indexFor(record: KeyModel) { return this.list.indexOf(record); } 55 | } 56 | -------------------------------------------------------------------------------- /examples/preboot/preboot.css: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * This is a same CSS file for overlay and spinner. This can/should 4 | * be overriden/customized to suit your needs 5 | */ 6 | 7 | .preboot-overlay { 8 | background: grey; 9 | opacity: .27; 10 | } 11 | 12 | @keyframes spin { 13 | to { transform: rotate(1turn); } 14 | } 15 | 16 | .preboot-spinner { 17 | position: relative; 18 | display: inline-block; 19 | width: 5em; 20 | height: 5em; 21 | margin: 0 .5em; 22 | font-size: 12px; 23 | text-indent: 999em; 24 | overflow: hidden; 25 | animation: spin 1s infinite steps(8); 26 | } 27 | 28 | .preboot-spinner.small { 29 | font-size: 6px; 30 | } 31 | 32 | .preboot-spinner.large { 33 | font-size: 24px; 34 | } 35 | 36 | .preboot-spinner:before, 37 | .preboot-spinner:after, 38 | .preboot-spinner > div:before, 39 | .preboot-spinner > div:after { 40 | content: ''; 41 | position: absolute; 42 | top: 0; 43 | left: 2.25em; /* (container width - part width)/2 */ 44 | width: .5em; 45 | height: 1.5em; 46 | border-radius: .2em; 47 | background: #eee; 48 | box-shadow: 0 3.5em #eee; /* container height - part height */ 49 | transform-origin: 50% 2.5em; /* container height / 2 */ 50 | } 51 | 52 | .preboot-spinner:before { 53 | background: #555; 54 | } 55 | 56 | .preboot-spinner:after { 57 | transform: rotate(-45deg); 58 | background: #777; 59 | } 60 | 61 | .preboot-spinner > div:before { 62 | transform: rotate(-90deg); 63 | background: #999; 64 | } 65 | 66 | .preboot-spinner > div:after { 67 | transform: rotate(-135deg); 68 | background: #bbb; 69 | } 70 | -------------------------------------------------------------------------------- /modules/universal/server/src/stringifyElement.ts: -------------------------------------------------------------------------------- 1 | // dom closure 2 | import {Parse5DomAdapter} from 'angular2/src/platform/server/parse5_adapter'; 3 | Parse5DomAdapter.makeCurrent(); 4 | 5 | import {ListWrapper, MapWrapper} from 'angular2/src/facade/collection'; 6 | import {DOM} from 'angular2/src/platform/dom/dom_adapter'; 7 | import {isPresent, isString, StringWrapper} from 'angular2/src/facade/lang'; 8 | 9 | var _singleTagWhitelist = ['br', 'hr', 'input']; 10 | export function stringifyElement(el): string { 11 | var result = ''; 12 | if (DOM.isElementNode(el)) { 13 | var tagName = DOM.tagName(el).toLowerCase(); 14 | // Opening tag 15 | result += `<${tagName}`; 16 | // Attributes in an ordered way 17 | var attributeMap = DOM.attributeMap(el); 18 | var keys = []; 19 | attributeMap.forEach((v, k) => { keys.push(k); }); 20 | keys.sort(); 21 | for (let i = 0; i < keys.length; i++) { 22 | var key = keys[i]; 23 | var attValue = attributeMap.get(key); 24 | if (!isString(attValue)) { 25 | result += ` ${key}`; 26 | } else { 27 | result += ` ${key}="${attValue}"`; 28 | } 29 | } 30 | result += '>'; 31 | // Children 32 | var children = DOM.childNodes(DOM.templateAwareRoot(el)); 33 | for (let j = 0; j < children.length; j++) { 34 | result += stringifyElement(children[j]); 35 | } 36 | // Closing tag 37 | if (!ListWrapper.contains(_singleTagWhitelist, tagName)) { 38 | result += ``; 39 | } 40 | } else if (DOM.isCommentNode(el)) { 41 | result += ``; 42 | } else { 43 | result += DOM.getText(el); 44 | } 45 | return result; 46 | } 47 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.12' 4 | - '4' 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | - bower_components 10 | - angular/node_modules 11 | - angular/bower_components 12 | - web_modules 13 | - tsd_typings 14 | 15 | notifications: 16 | webhooks: 17 | urls: 18 | - https://webhooks.gitter.im/e/8953131954d5e41f649b 19 | on_success: always # options: [always|never|change] default: always 20 | on_failure: always # options: [always|never|change] default: always 21 | on_start: false # default: false 22 | slack: 23 | secure: EP4MzZ8JMyNQJ4S3cd5LEPWSMjC7ZRdzt3veelDiOeorJ6GwZfCDHncR+4BahDzQAuqyE/yNpZqaLbwRWloDi15qIUsm09vgl/1IyNky1Sqc6lEknhzIXpWSalo4/T9ZP8w870EoDvM/UO+LCV99R3wS8Nm9o99eLoWVb2HIUu0= 24 | 25 | env: 26 | global: 27 | - SAUCE_ACCESS_KEY='' 28 | # Token for tsd to increase github rate limit 29 | # See https://github.com/DefinitelyTyped/tsd#tsdrc 30 | # This does not use http://docs.travis-ci.com/user/environment-variables/#Secure-Variables 31 | # because those are not visible for pull requests, and those should also be reliable. 32 | # This SSO token belongs to github account angular-github-ratelimit-token which has no access 33 | # (password is in Valentine) 34 | - TSDRC='{"token":"ef474500309daea53d5991b3079159a29520a40b"}' 35 | # GITHUB_TOKEN_ANGULAR 36 | - secure: "fq/U7VDMWO8O8SnAQkdbkoSe2X92PVqg4d044HmRYVmcf6YbO48+xeGJ8yOk0pCBwl3ISO4Q2ot0x546kxfiYBuHkZetlngZxZCtQiFT9kyId8ZKcYdXaIW9OVdw3Gh3tQyUwDucfkVhqcs52D6NZjyE2aWZ4/d1V4kWRO/LMgo=" 37 | 38 | before_install: 39 | - echo ${TSDRC} > .tsdrc 40 | # - npm install -g webpack 41 | - npm install -g tsd 42 | 43 | before_script: 44 | - npm install -g gulp 45 | 46 | script: 47 | - npm run ci 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var SERVER_IP = '127.0.0.1'; 2 | 3 | var port = process.env.PORT || 3000; 4 | // var ssl = process.env.SSLPORT || 4000; 5 | 6 | // Module dependencies 7 | var http = require('http'); 8 | // var https = require('https'); 9 | 10 | /* 11 | var options = { 12 | key: fs.readFileSync('/private/etc/apache2/ssl/ssl.key'), 13 | cert: fs.readFileSync('/private/etc/apache2/ssl/ssl.crt') 14 | }; 15 | */ 16 | 17 | var server = require('./dist/examples/app/server/server')(__dirname); 18 | 19 | // Start server 20 | module.exports.Server = http.createServer(server).listen(port, SERVER_IP, function() { 21 | console.log('Listening on port: ' + port); 22 | // for smoke testing 23 | // smokeTest(); 24 | }); 25 | /* 26 | https.createServer(options, server).listen(ssl, function() { 27 | console.log('Listening on port: ' + ssl + ' in ' + process.env.NODE_ENV); 28 | }); 29 | */ 30 | 31 | 32 | function smokeTest() { 33 | var req = http.get({ 34 | host: 'localhost', 35 | port: 3000, 36 | path: '/?server=true&client=false&preboot=false&bootstrap=false', 37 | }, function(res) { 38 | // console.log('STATUS: ' + res.statusCode); 39 | // console.log('HEADERS: ' + JSON.stringify(res.headers, null, 2)); 40 | 41 | // Buffer the body entirely for processing as a whole. 42 | var bodyChunks = []; 43 | res. 44 | on('data', function(chunk) { 45 | // You can process streamed parts here... 46 | bodyChunks.push(chunk); 47 | }). 48 | on('end', function() { 49 | var body = Buffer.concat(bodyChunks); 50 | // console.log('GOOD' /*, body.toString()*/ ); 51 | // ...and/or process the entire body here. 52 | }) 53 | }); 54 | 55 | req.on('error', function(e) { 56 | console.error('ERROR: ' + e.message); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /modules/universal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "noImplicitAny": false, 7 | "removeComments": false, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true, 12 | "listFiles": false, 13 | "outDir": "dist" 14 | }, 15 | "exclude": [ 16 | "client/test", 17 | "server/test", 18 | "node_modules" 19 | ], 20 | "files": [ 21 | "../../tsd_typings/tsd.d.ts", 22 | "../../custom_typings/_custom.d.ts", 23 | "universal.d.ts", 24 | "server/index.ts", 25 | "server/server.ts", 26 | "server/src/express/engine.ts", 27 | "server/src/directives/server_form.ts", 28 | "server/src/http/server_http.ts", 29 | "server/src/router/server_router.ts", 30 | "server/src/render/server_dom_renderer.ts", 31 | "server/src/ng_scripts.ts", 32 | "server/src/helper.ts", 33 | "server/src/render.ts", 34 | "server/src/server_patch.ts", 35 | "server/src/stringifyElement.ts", 36 | "server/test/router_server_spec.ts", 37 | "client/index.ts", 38 | "client/client.ts", 39 | "client/src/ng_preload_cache.ts", 40 | "client/src/bootstrap.ts" 41 | ], 42 | "formatCodeOptions": { 43 | "indentSize": 2, 44 | "tabSize": 2, 45 | "newLineCharacter": "\r\n", 46 | "convertTabsToSpaces": true, 47 | "insertSpaceAfterCommaDelimiter": true, 48 | "insertSpaceAfterSemicolonInForStatements": true, 49 | "insertSpaceBeforeAndAfterBinaryOperators": true, 50 | "insertSpaceAfterKeywordsInControlFlowStatements": true, 51 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, 52 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 53 | "placeOpenBraceOnNewLineForFunctions": false, 54 | "placeOpenBraceOnNewLineForControlBlocks": false 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 2 | exports.config = { 3 | baseUrl: 'http://localhost:3000', 4 | 5 | specs: [ 6 | 'test/*.e2e.js' 7 | ], 8 | 9 | allScriptsTimeout: 11000, 10 | 11 | framework: 'jasmine2', 12 | 13 | jasmineNodeOpts: { 14 | defaultTimeoutInterval: 60000, 15 | showTiming: true, 16 | print: function(){} // remove the dots!! 17 | }, 18 | 19 | capabilities: { 20 | browserName: 'chrome', 21 | chromeOptions: { 22 | //Important for benchpress to get timeline data from the browser 23 | 'args': ['--js-flags=--expose-gc'], 24 | 'perfLoggingPrefs': { 25 | 'traceCategories': 'blink.console,disabled-by-default-devtools.timeline' 26 | } 27 | }, 28 | loggingPrefs: { 29 | performance: 'ALL' 30 | } 31 | }, 32 | 33 | // https://github.com/mllrsohn/gulp-protractor#protractor-webdriver 34 | seleniumServerJar: './node_modules/protractor/selenium/selenium-server-standalone-2.47.1.jar', 35 | //seleniumAddress: 'http://localhost:4444/wd/hub', 36 | 37 | onPrepare: function() { 38 | browser.manage().deleteAllCookies(); 39 | browser.ignoreSynchronization = true; 40 | 41 | var SpecReporter = require('jasmine-spec-reporter'); 42 | jasmine.getEnv().addReporter(new SpecReporter({ 43 | displayStacktrace:'none', 44 | displaySpecDuration: true, 45 | verbose: 1, 46 | showStack: true, 47 | color: true 48 | })); 49 | 50 | /* 51 | // open a new browser for every benchmark 52 | var originalBrowser = browser; 53 | var _tmpBrowser; 54 | beforeEach(function() { 55 | global.browser = originalBrowser.forkNewDriverInstance(); 56 | global.element = global.browser.element; 57 | global.$ = global.browser.$; 58 | global.$$ = global.browser.$$; 59 | global.browser.ignoreSynchronization = true; 60 | }); 61 | afterEach(function() { 62 | global.browser.quit(); 63 | global.browser = originalBrowser; 64 | }); 65 | */ 66 | } 67 | 68 | }; 69 | -------------------------------------------------------------------------------- /examples/app/universal/todo/todo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 15 | 16 |
17 | 18 | 19 | 20 |
    21 | 22 |
  • 23 | 24 |
    26 | 27 | 30 | 31 | 32 | 33 | 34 |
    35 | 36 |
    37 | 38 | 42 | 43 |
    44 | 45 |
  • 46 |
47 | 48 |
49 | 50 |
51 | 52 |
53 | 64 | 65 |
66 | 67 |
68 | 69 | 73 | -------------------------------------------------------------------------------- /modules/universal/server/src/helper.ts: -------------------------------------------------------------------------------- 1 | export function escapeRegExp(str): string { 2 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); 3 | } 4 | 5 | export function stringify(obj, replacer = null, spaces = 2): string { 6 | return JSON.stringify(obj, replacer, spaces); 7 | } 8 | 9 | export function cssHyphenate(propertyName: string): string { 10 | return propertyName.replace(/([A-Z])/g, '-$1') 11 | .replace(/^ms-/, '-ms-') // Internet Explorer vendor prefix. 12 | .toLowerCase(); 13 | } 14 | 15 | export function showDebug(options = {}): string { 16 | var info = '\n'; 17 | for (var prop in options) { 18 | if (prop && options[prop]) { 19 | info += '' + 20 | '
' +
21 |       `${ prop } = ${ stringify(options[prop]) }` +
22 |       '
'; 23 | } 24 | } 25 | return info; 26 | } 27 | 28 | export function stringToBoolean(txt) { 29 | if (typeof txt !== 'string') { return txt; } 30 | switch (txt.toLowerCase()) { 31 | case'false': case'\'false\'': case'"false"': case'0': case'no': return false; 32 | case'true': case'\'true\'': case'"true"': case'1': case'yes': return true; 33 | default: return txt; 34 | } 35 | } 36 | 37 | export function queryParamsToBoolean(query) { 38 | var obj = {}; 39 | for (let prop in query) { 40 | if (query.hasOwnProperty(prop)) { 41 | obj[prop] = stringToBoolean(query[prop]); 42 | } 43 | } 44 | return obj; 45 | } 46 | 47 | 48 | export function selectorRegExpFactory(selector: string): RegExp { 49 | /* 50 | $1 $2 $3 51 | content 52 | /<([^\s\>]+)[^>]*>([\s\S]*?)<\/\1>/ 53 | */ 54 | 55 | let regExpSelect = `<${ escapeRegExp(selector) }[^>]*>([\\s\\S]*?)<\/${ escapeRegExp(selector) }>`; 56 | return new RegExp(regExpSelect); 57 | } 58 | 59 | export function arrayFlattenTree(children: any[], arr: any[]): any[] { 60 | for (let child of children) { 61 | arr.push(child.res); 62 | arrayFlattenTree(child.children, arr); 63 | } 64 | return arr 65 | } 66 | 67 | -------------------------------------------------------------------------------- /modules/preboot/src/client/freeze/freeze_with_spinner.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {PrebootOptions} from '../../interfaces/preboot_options'; 3 | 4 | // overlay and spinner nodes stored in memory in between prep and cleanup 5 | export let state = { 6 | overlay: null, 7 | spinner: null 8 | }; 9 | 10 | /** 11 | * Clean up the freeze elements from the DOM 12 | */ 13 | export function cleanup(preboot: PrebootRef) { 14 | preboot.dom.removeNode(state.overlay); 15 | preboot.dom.removeNode(state.spinner); 16 | 17 | state.overlay = null; 18 | state.spinner = null; 19 | } 20 | 21 | /** 22 | * Prepare for freeze by adding elements to the DOM and adding an event handler 23 | */ 24 | export function prep(preboot: PrebootRef, opts: PrebootOptions) { 25 | let freezeOpts = opts.freeze || {}; 26 | let freezeStyles = freezeOpts.styles || {}; 27 | let overlayStyles = freezeStyles.overlay || {}; 28 | let spinnerStyles = freezeStyles.spinner || {}; 29 | 30 | // add the overlay and spinner to the end of the body 31 | state.overlay = preboot.dom.addNodeToBody('div', overlayStyles.className, overlayStyles.style); 32 | state.spinner = preboot.dom.addNodeToBody('div', spinnerStyles.className, spinnerStyles.style); 33 | 34 | // when a freeze event occurs, show the overlay and spinner 35 | preboot.dom.on(freezeOpts.eventName, function () { 36 | 37 | // if there is an active node, position spinner on top of it and blur the focus 38 | let activeNode = preboot.activeNode; 39 | if (activeNode) { 40 | state.spinner.style.top = activeNode.offsetTop; 41 | state.spinner.style.left = activeNode.offsetLeft; 42 | 43 | if (freezeOpts.doBlur) { 44 | activeNode.blur(); 45 | } 46 | } 47 | 48 | // display the overlay and spinner 49 | state.overlay.style.display = 'block'; 50 | state.spinner.style.display = 'block'; 51 | 52 | // preboot should end in under 5 seconds, but if it doesn't unfreeze just in case 53 | setTimeout(() => cleanup(preboot), freezeOpts.timeout); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /modules/preboot/src/client/log.ts: -------------------------------------------------------------------------------- 1 | import {PrebootOptions} from '../interfaces/preboot_options'; 2 | import {PrebootEvent} from '../interfaces/event'; 3 | import {Element} from '../interfaces/element'; 4 | 5 | function logOptions(opts: PrebootOptions) { 6 | console.log('preboot options are:'); 7 | console.log(opts); 8 | } 9 | 10 | function logEvents(events: PrebootEvent[]) { 11 | console.log('preboot events captured are:'); 12 | console.log(events); 13 | } 14 | 15 | function replaySuccess(serverNode: Element, clientNode: Element, event: any) { 16 | console.log('replaying:'); 17 | console.log({ 18 | serverNode: serverNode, 19 | clientNode: clientNode, 20 | event: event 21 | }); 22 | } 23 | 24 | function missingClientNode(serverNode: Element) { 25 | console.log('preboot could not find client node for:'); 26 | console.log(serverNode); 27 | } 28 | 29 | function remainingEvents(events: PrebootEvent[]) { 30 | if (events && events.length) { 31 | console.log('the following events were not replayed:'); 32 | console.log(events); 33 | } 34 | } 35 | 36 | function noRefocus(serverNode: Element) { 37 | console.log('Could not find node on client to match server node for refocus:'); 38 | console.log(serverNode); 39 | } 40 | 41 | let logMap = { 42 | '1': logOptions, 43 | '2': logEvents, 44 | '3': replaySuccess, 45 | '4': missingClientNode, 46 | '5': remainingEvents, 47 | '6': noRefocus 48 | }; 49 | 50 | /** 51 | * Idea here is simple. If debugging turned on and this module exists, we 52 | * log various things that happen in preboot. The calling code only references 53 | * a number (keys in logMap) to a handling function. By doing this, we are 54 | * able to cut down on the amount of logging code in preboot when no in debug mode. 55 | */ 56 | export function log(...args) { 57 | if (!args.length) { return; } 58 | 59 | let id = args[0] + ''; 60 | let fn = logMap[id]; 61 | 62 | if (fn) { 63 | fn(...args.slice(1)); 64 | } else { 65 | console.log('log: ' + JSON.stringify(args)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/preboot/test/client/listen/listen_by_event_bindings_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {state, walkDOM, addNodeEvents, getNodeEvents} from '../../../src/client/listen/listen_by_event_bindings'; 4 | 5 | describe('listen_by_event_bindings', function () { 6 | describe('walkDOM', function () { 7 | 8 | it('should not do anything if no node passed in', function () { 9 | walkDOM(null, null); 10 | }); 11 | 12 | it('should walk a fake DOM', function () { 13 | let node4 = {}; 14 | let node3 = { nextSibling: node4 }; 15 | let node2 = { nextSibling: node3 }; 16 | let node1 = { firstChild: node2 }; 17 | let obj = { cb: function () {} }; 18 | 19 | spyOn(obj, 'cb'); 20 | 21 | walkDOM(node1, obj.cb); 22 | 23 | expect(obj.cb).toHaveBeenCalledWith(node1); 24 | expect(obj.cb).toHaveBeenCalledWith(node2); 25 | expect(obj.cb).toHaveBeenCalledWith(node3); 26 | expect(obj.cb).toHaveBeenCalledWith(node4); 27 | }); 28 | 29 | }); 30 | 31 | describe('addNodeEvents', function () { 32 | it('should not do anything with no attrs', function () { 33 | let node = {}; 34 | addNodeEvents(node); 35 | expect(node).toEqual({}); 36 | }); 37 | 38 | it('should add node events', function () { 39 | let node = { 40 | attributes: [ 41 | { name: '(click)' }, 42 | { name: 'zoo' }, 43 | { name: 'on-foo' } 44 | ] 45 | }; 46 | let expected = [ 47 | { node: node, eventName: 'click' }, 48 | { node: node, eventName: 'foo' } 49 | ]; 50 | addNodeEvents(node); 51 | expect(state.nodeEvents).toEqual(expected); 52 | }); 53 | }); 54 | 55 | describe('getNodeEvents()', function () { 56 | it('should return an empty array if no body', function () { 57 | let preboot = { 58 | dom: { 59 | state: {} 60 | } 61 | }; 62 | let strategy = {}; 63 | let expected = []; 64 | let actual = getNodeEvents(preboot, strategy); 65 | expect(actual).toEqual(expected); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /modules/preboot/src/client/listen/listen_by_event_bindings.ts: -------------------------------------------------------------------------------- 1 | import {PrebootRef} from '../../interfaces/preboot_ref'; 2 | import {ListenStrategy} from '../../interfaces/strategy'; 3 | import {NodeEvent} from '../../interfaces/event'; 4 | 5 | // regex for how events defined in Angular 2 templates; for example: 6 | //
7 | //
8 | const ngEventPattern = /^(on\-.*)|(\(.*\))$/; 9 | 10 | // state for this module just includes the nodeEvents (exported for testing purposes) 11 | export let state = { nodeEvents: [] }; 12 | 13 | /** 14 | * This is from Crockford to walk the DOM (http://whlp.ly/1Ii6YbR). 15 | * Recursively walk DOM tree and execute input param function at 16 | * each node. 17 | */ 18 | export function walkDOM(node: any, func: Function) { 19 | if (!node) { return; } 20 | 21 | func(node); 22 | node = node.firstChild; 23 | while (node) { 24 | walkDOM(node, func); 25 | node = node.nextSibling; 26 | } 27 | } 28 | 29 | /** 30 | * This is function called at each node while walking DOM. 31 | * Will add node event if events defined on element. 32 | */ 33 | export function addNodeEvents(node: any) { 34 | let attrs = node.attributes; 35 | 36 | // if no attributes, return without doing anything 37 | if (!attrs) { return; } 38 | 39 | // otherwise loop through attributes to try and find an Angular 2 event binding 40 | for (let attr of attrs) { 41 | let name = attr.name; 42 | 43 | // if attribute name is an Angular 2 event binding 44 | if (ngEventPattern.test(name)) { 45 | 46 | // extract event name from the () or on- (TODO: replace this w regex) 47 | name = name.charAt(0) === '(' ? 48 | name.substring(1, name.length - 1) : // remove parenthesis 49 | name.substring(3); // remove on- 50 | 51 | state.nodeEvents.push({ 52 | node: node, 53 | eventName: name 54 | }); 55 | } 56 | } 57 | } 58 | 59 | /** 60 | * This listen strategy will look for a specific attribute which contains all the elements 61 | * that a given element is listening to. 62 | */ 63 | export function getNodeEvents(preboot: PrebootRef, strategy: ListenStrategy): NodeEvent[] { 64 | state.nodeEvents = []; 65 | walkDOM(preboot.dom.state.body, addNodeEvents); 66 | return state.nodeEvents; 67 | } 68 | -------------------------------------------------------------------------------- /modules/preboot/src/client/buffer_manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The purpose of this module is to manage the buffering of client rendered 3 | * HTML to a hidden div. After the client is fully bootstrapped, this module 4 | * would then be used to switch the hidden client div and the visible server div. 5 | * Note that this technique would only work if the app root is somewhere within 6 | * the body tag in the HTML document. 7 | */ 8 | import {PrebootRef} from '../interfaces/preboot_ref'; 9 | 10 | // expose state for testing purposes 11 | export let state = { switched: false }; 12 | 13 | /** 14 | * Create a second div that will be the client root for an app 15 | */ 16 | export function prep(preboot: PrebootRef) { 17 | 18 | // server root is the app root when we get started 19 | let serverRoot = preboot.dom.state.appRoot; 20 | 21 | // client root is going to be a shallow clone of the server root 22 | let clientRoot = serverRoot.cloneNode(false); 23 | 24 | // client in the DOM, but not displayed until time for switch 25 | clientRoot.style.display = 'none'; 26 | 27 | // insert the client root right before the server root 28 | serverRoot.parentNode.insertBefore(clientRoot, serverRoot); 29 | 30 | // update the dom manager to store the server and client roots (first param is appRoot) 31 | preboot.dom.updateRoots(serverRoot, serverRoot, clientRoot); 32 | } 33 | 34 | /** 35 | * We want to simultaneously remove the server node from the DOM 36 | * and display the client node 37 | */ 38 | export function switchBuffer(preboot: PrebootRef) { 39 | let domState = preboot.dom.state; 40 | 41 | // get refs to the roots 42 | let clientRoot = domState.clientRoot || domState.appRoot; 43 | let serverRoot = domState.serverRoot || domState.appRoot; 44 | 45 | // don't do anything if already switched 46 | if (state.switched) { return; } 47 | 48 | // remove the server root if not same as client and not the body 49 | if (serverRoot !== clientRoot && serverRoot.nodeName !== 'BODY') { 50 | preboot.dom.removeNode(serverRoot); 51 | } 52 | 53 | // display the client 54 | clientRoot.style.display = 'block'; 55 | 56 | // update the roots; first param is the new appRoot; serverRoot now null 57 | preboot.dom.updateRoots(clientRoot, null, clientRoot); 58 | 59 | // finally mark state as switched 60 | state.switched = true; 61 | } 62 | -------------------------------------------------------------------------------- /examples/app/server/api.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | var util = require('util'); 4 | var {Router} = require('express'); 5 | 6 | 7 | var COUNT = 4; 8 | var TODOS = [ 9 | { id: 0, value: 'finish example', created_at: new Date(), completed: false }, 10 | { id: 1, value: 'add tests', created_at: new Date(), completed: false }, 11 | { id: 2, value: 'include development environment', created_at: new Date(), completed: false }, 12 | { id: 3, value: 'include production environment', created_at: new Date(), completed: false } 13 | ]; 14 | module.exports = function(ROOT) { 15 | 16 | var router = Router(); 17 | 18 | router.route('/todos') 19 | .get(function(req, res) { 20 | console.log('GET'); 21 | // 70ms latency 22 | setTimeout(function() { 23 | res.json(TODOS); 24 | }, 0); 25 | 26 | }) 27 | .post(function(req, res) { 28 | console.log('POST', util.inspect(req.body, {colors: true})); 29 | var todo = req.body; 30 | if (todo) { 31 | TODOS.push({ 32 | value: todo.value, 33 | created_at: new Date(), 34 | completed: todo.completed, 35 | id: COUNT++ 36 | }); 37 | return res.json(todo); 38 | } 39 | 40 | return res.end(); 41 | }); 42 | 43 | router.param('todo_id', function(req, res, next, todo_id) { 44 | // ensure correct prop type 45 | var id = Number(req.params.todo_id); 46 | try { 47 | var todo = TODOS[id]; 48 | req.todo_id = id; 49 | req.todo = TODOS[id]; 50 | next(); 51 | } catch (e) { 52 | next(new Error('failed to load todo')); 53 | } 54 | }); 55 | 56 | router.route('/todos/:todo_id') 57 | .get(function(req, res) { 58 | console.log('GET', util.inspect(req.todo, {colors: true})); 59 | 60 | res.json(req.todo); 61 | }) 62 | .put(function(req, res) { 63 | console.log('PUT', util.inspect(req.body, {colors: true})); 64 | 65 | var index = TODOS.indexOf(req.todo); 66 | var todo = TODOS[index] = req.body; 67 | 68 | res.json(todo); 69 | }) 70 | .delete(function(req, res) { 71 | console.log('DELETE', req.todo_id); 72 | 73 | var index = TODOS.indexOf(req.todo); 74 | TODOS.splice(index, 1); 75 | 76 | res.json(req.todo); 77 | }); 78 | 79 | return router; 80 | }; 81 | -------------------------------------------------------------------------------- /modules/preboot/src/server/presets.ts: -------------------------------------------------------------------------------- 1 | import {PrebootOptions} from '../interfaces/preboot_options'; 2 | 3 | export default { 4 | 5 | /** 6 | * Record key strokes in all textboxes and textareas as well as changes 7 | * in other form elements like checkboxes, radio buttons and select dropdowns 8 | */ 9 | keyPress: (opts: PrebootOptions) => { 10 | opts.listen = opts.listen || []; 11 | opts.listen.push({ 12 | name: 'selectors', 13 | eventsBySelector: { 14 | 'input,textarea': ['keypress', 'keyup', 'keydown'] 15 | } 16 | }); 17 | opts.listen.push({ 18 | name: 'selectors', 19 | eventsBySelector: { 20 | 'input[type="checkbox"],input[type="radio"],select,option': ['change'] 21 | } 22 | }); 23 | }, 24 | 25 | /** 26 | * For focus option, the idea is to track focusin and focusout 27 | */ 28 | focus: (opts: PrebootOptions) => { 29 | opts.listen = opts.listen || []; 30 | opts.listen.push({ 31 | name: 'selectors', 32 | eventsBySelector: { 33 | 'input,textarea': ['focusin', 'focusout', 'mousedown', 'mouseup'] 34 | }, 35 | trackFocus: true, 36 | doNotReplay: true 37 | }); 38 | }, 39 | 40 | /** 41 | * This option used for button press events 42 | */ 43 | buttonPress: (opts: PrebootOptions) => { 44 | opts.listen = opts.listen || []; 45 | opts.listen.push({ 46 | name: 'selectors', 47 | preventDefault: true, 48 | eventsBySelector: { 49 | 'input[type="submit"],button': ['click'] 50 | }, 51 | dispatchEvent: opts.freeze && opts.freeze.eventName 52 | }); 53 | }, 54 | 55 | /** 56 | * This option will pause preboot and bootstrap processes 57 | * if focus on an input textbox or textarea 58 | */ 59 | pauseOnTyping: (opts: PrebootOptions) => { 60 | opts.listen = opts.listen || []; 61 | opts.listen.push({ 62 | name: 'selectors', 63 | eventsBySelector: { 64 | 'input': ['focus'], 65 | 'textarea': ['focus'] 66 | }, 67 | doNotReplay: true, 68 | dispatchEvent: opts.pauseEvent 69 | }); 70 | 71 | opts.listen.push({ 72 | name: 'selectors', 73 | eventsBySelector: { 74 | 'input': ['blur'], 75 | 'textarea': ['blur'] 76 | }, 77 | doNotReplay: true, 78 | dispatchEvent: opts.resumeEvent 79 | }); 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /modules/universal/server/src/ng_preboot.ts: -------------------------------------------------------------------------------- 1 | 2 | export function prebootConfigDefault(config) { 3 | return (Object).assign({ 4 | start: true, 5 | appRoot: 'app', // selector for root element 6 | replay: 'rerender', // rerender replay strategy 7 | buffer: true, // client app will write to hidden div until bootstrap complete 8 | debug: false, 9 | uglify: true, 10 | presets: ['keyPress', 'buttonPress', 'focus'] 11 | }, config || {}); 12 | } 13 | 14 | export function getPrebootCSS(): string { 15 | return ` 16 | .preboot-overlay { 17 | background: grey; 18 | opacity: .27; 19 | } 20 | 21 | @keyframes spin { 22 | to { transform: rotate(1turn); } 23 | } 24 | 25 | .preboot-spinner { 26 | position: relative; 27 | display: inline-block; 28 | width: 5em; 29 | height: 5em; 30 | margin: 0 .5em; 31 | font-size: 12px; 32 | text-indent: 999em; 33 | overflow: hidden; 34 | animation: spin 1s infinite steps(8); 35 | } 36 | 37 | .preboot-spinner.small { 38 | font-size: 6px; 39 | } 40 | 41 | .preboot-spinner.large { 42 | font-size: 24px; 43 | } 44 | 45 | .preboot-spinner:before, 46 | .preboot-spinner:after, 47 | .preboot-spinner > div:before, 48 | .preboot-spinner > div:after { 49 | content: ''; 50 | position: absolute; 51 | top: 0; 52 | left: 2.25em; /* (container width - part width)/2 */ 53 | width: .5em; 54 | height: 1.5em; 55 | border-radius: .2em; 56 | background: #eee; 57 | box-shadow: 0 3.5em #eee; /* container height - part height */ 58 | transform-origin: 50% 2.5em; /* container height / 2 */ 59 | } 60 | 61 | .preboot-spinner:before { 62 | background: #555; 63 | } 64 | 65 | .preboot-spinner:after { 66 | transform: rotate(-45deg); 67 | background: #777; 68 | } 69 | 70 | .preboot-spinner > div:before { 71 | transform: rotate(-90deg); 72 | background: #999; 73 | } 74 | 75 | .preboot-spinner > div:after { 76 | transform: rotate(-135deg); 77 | background: #bbb; 78 | } 79 | ` 80 | } 81 | 82 | 83 | export function createPrebootHTML(code: string, config?: any): string { 84 | let html = ''; 85 | 86 | html += ` 87 | 90 | `; 91 | 92 | html += ` 93 | 96 | `; 97 | 98 | if (config && config.start === true) { 99 | html += ''; 100 | } 101 | 102 | return html; 103 | } 104 | -------------------------------------------------------------------------------- /modules/universal/README.md: -------------------------------------------------------------------------------- 1 | ![Angular 2 Universal](https://cloud.githubusercontent.com/assets/1016365/10639063/138338bc-7806-11e5-8057-d34c75f3cafc.png) 2 | 3 | # Angular 2 Universal 4 | > Universal (isomorphic) JavaScript support for Angular 2 5 | 6 | # Table of Contents 7 | * [Modules](#modules) 8 | * [Universal](#universal) 9 | * [preboot.js](#prebootjs) 10 | * [Best Practices](#best-practices) 11 | * [What's in a name?](#whats-in-a-name) 12 | * [License](#license) 13 | 14 | # Modules 15 | 16 | ## Universal 17 | > Manage your application lifecycle and serialize changes while on the server to be sent to the client 18 | 19 | ### Documentation 20 | [Design Doc](https://docs.google.com/document/d/1q6g9UlmEZDXgrkY88AJZ6MUrUxcnwhBGS0EXbVlYicY) 21 | 22 | ### Videos 23 | Full Stack Angular 2 - AngularConnect, Oct 2015 24 | [![Full Stack Angular 2](https://img.youtube.com/vi/MtoHFDfi8FM/0.jpg)](https://www.youtube.com/watch?v=MtoHFDfi8FM) 25 | 26 | Angular 2 Server Rendering - Angular U, July 2015 27 | [![Angular 2 Server Rendering](http://img.youtube.com/vi/0wvZ7gakqV4/0.jpg)](http://www.youtube.com/watch?v=0wvZ7gakqV4) 28 | 29 | ## preboot.js 30 | > Control server-rendered page and transfer state before client-side web app loads to the client-side-app. 31 | 32 | # Best Practices 33 | > When building Universal components in Angular 2 there are a few things to keep in mind 34 | 35 | * Know the difference between attributes and properties in relation to the DOM 36 | * Don't manipulate the `nativeElement` directly. Use the `Renderer` 37 | ```typescript 38 | constructor(element: ElementRef, renderer: Renderer) { 39 | renderer.setElementStyle(element, 'fontSize', 'x-large'); 40 | } 41 | ``` 42 | * Don't use any of the browser types provided in the global namespace such as `navigator` or `document`. Anything outside of Angular will not be detected when serializing your application into html 43 | * Keep your directives stateless as much as possible. For stateful directives you may need to provide an attribute that reflects the corresponding property with an initial string value such as `url` in `img` tag. For our native `` element the `src` attribute is reflected as the `src` property of the element type `HTMLImageElement`. 44 | 45 | # What's in a name? 46 | We believe that using the word "universal" is correct when referring to a JavaScript Application that runs in more environments than the browser. (inspired by [Universal JavaScript](https://medium.com/@mjackson/universal-javascript-4761051b7ae9)) 47 | 48 | # License 49 | [Apache-2.0](/LICENSE) 50 | -------------------------------------------------------------------------------- /examples/preboot/preboot_example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 33 | 36 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /modules/universal/server/src/router/server_router.ts: -------------------------------------------------------------------------------- 1 | import * as nodeUrl from 'url'; 2 | import {Injectable, Inject, provide} from 'angular2/core'; 3 | import {LocationStrategy} from 'angular2/router'; 4 | import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy'; 5 | import {BASE_URL} from '../http/server_http'; 6 | 7 | // TODO: see https://github.com/angular/universal/issues/60#issuecomment-130463593 8 | class MockServerHistory implements History { 9 | length: number; 10 | state: any; 11 | constructor () {/*TODO*/} 12 | back(distance?: any): void {/*TODO*/} 13 | forward(distance?: any): void {/*TODO*/} 14 | go(delta?: any): void {/*TODO*/} 15 | pushState(statedata: any, title?: string, url?: string): void {/*TODO*/} 16 | replaceState(statedata: any, title?: string, url?: string): void {/*TODO*/} 17 | } 18 | 19 | class MockServerLocation implements Location { 20 | hash: string; 21 | host: string; 22 | hostname: string; 23 | href: string; 24 | origin: string; 25 | pathname: string; 26 | port: string; 27 | protocol: string; 28 | search: string; 29 | constructor () {/*TODO*/} 30 | assign(url: string): void { 31 | var parsed = nodeUrl.parse(url); 32 | this.hash = parsed.hash; 33 | this.host = parsed.host; 34 | this.hostname = parsed.hostname; 35 | this.href = parsed.href; 36 | this.pathname = parsed.pathname; 37 | this.port = parsed.port; 38 | this.protocol = parsed.protocol; 39 | this.search = parsed.search; 40 | this.origin = parsed.protocol + '//' + parsed.hostname + ':' + parsed.port; 41 | } 42 | reload(forcedReload?: boolean): void {/*TODO*/} 43 | replace(url: string): void { 44 | this.assign(url); 45 | } 46 | toString(): string { /*TODO*/ return ''; } 47 | } 48 | 49 | 50 | @Injectable() 51 | export class ServerLocationStrategy extends LocationStrategy { 52 | private _location: Location = new MockServerLocation(); 53 | private _history: History = new MockServerHistory(); 54 | private _baseHref: string = '/'; 55 | 56 | constructor(@Inject(BASE_URL) baseUrl: string) { 57 | super(); 58 | this._location.assign(baseUrl); 59 | } 60 | 61 | onPopState(fn: EventListener): void {/*TODO*/} 62 | 63 | getBaseHref(): string { return this._baseHref; } 64 | 65 | path(): string { return this._location.pathname; } 66 | 67 | pushState(state: any, title: string, url: string) {/*TODO*/} 68 | 69 | replaceState(state: any, title: string, url: string) {/*TODO*/} 70 | 71 | forward(): void { 72 | this._history.forward(); 73 | } 74 | 75 | back(): void { 76 | this._history.back(); 77 | } 78 | 79 | prepareExternalUrl(internal: string): string { return internal; } 80 | } 81 | 82 | export const SERVER_LOCATION_PROVIDERS: Array = [ 83 | provide(LocationStrategy, {useClass: ServerLocationStrategy}) 84 | ]; 85 | -------------------------------------------------------------------------------- /modules/preboot/test/client/replay/replay_after_hydrate_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {replayEvents} from '../../../src/client/replay/replay_after_hydrate'; 4 | 5 | describe('replay_after_hydrate', function () { 6 | describe('replayEvents()', function () { 7 | it('should do nothing and return empty array if no params', function () { 8 | let preboot = { dom: {} }; 9 | let strategy = {}; 10 | let events = []; 11 | let expected = []; 12 | let actual = replayEvents(preboot, strategy, events); 13 | expect(actual).toEqual(expected); 14 | }); 15 | 16 | it('should dispatch all events w/o checkIfExists', function () { 17 | let node1 = { name: 'node1', dispatchEvent: function (evt) {} }; 18 | let node2 = { name: 'node2', dispatchEvent: function (evt) {} }; 19 | let preboot = { 20 | dom: { 21 | appContains: function () { return false; } 22 | } 23 | }; 24 | let strategy = { 25 | checkIfExists: false 26 | }; 27 | let events = [ 28 | { name: 'evt1', event: { name: 'evt1' }, node: node1 }, 29 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 30 | ]; 31 | let expected = []; 32 | 33 | spyOn(node1, 'dispatchEvent'); 34 | spyOn(node2, 'dispatchEvent'); 35 | spyOn(preboot.dom, 'appContains'); 36 | 37 | let actual = replayEvents(preboot, strategy, events); 38 | expect(actual).toEqual(expected); 39 | expect(node1.dispatchEvent).toHaveBeenCalledWith(events[0].event); 40 | expect(node2.dispatchEvent).toHaveBeenCalledWith(events[1].event); 41 | expect(preboot.dom.appContains).not.toHaveBeenCalled(); 42 | }); 43 | 44 | it('should checkIfExists and only dispatch on 1 node, return other', function () { 45 | let node1 = { name: 'node1', dispatchEvent: function (evt) {} }; 46 | let node2 = { name: 'node2', dispatchEvent: function (evt) {} }; 47 | let preboot = { 48 | dom: { 49 | appContains: function (node) { 50 | return node.name === 'node1'; 51 | } 52 | } 53 | }; 54 | let strategy = { 55 | checkIfExists: true 56 | }; 57 | let events = [ 58 | { name: 'evt1', event: { name: 'evt1' }, node: node1 }, 59 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 60 | ]; 61 | let expected = [ 62 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 63 | ]; 64 | 65 | spyOn(node1, 'dispatchEvent'); 66 | spyOn(node2, 'dispatchEvent'); 67 | spyOn(preboot.dom, 'appContains').and.callThrough(); 68 | 69 | let actual = replayEvents(preboot, strategy, events); 70 | expect(actual).toEqual(expected); 71 | expect(node1.dispatchEvent).toHaveBeenCalledWith(events[0].event); 72 | expect(node2.dispatchEvent).not.toHaveBeenCalled(); 73 | expect(preboot.dom.appContains).toHaveBeenCalledWith(node1); 74 | expect(preboot.dom.appContains).toHaveBeenCalledWith(node2); 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /modules/universal/server/src/render/server_dom_renderer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isPresent, 3 | stringify 4 | } from 'angular2/src/facade/lang'; 5 | import { 6 | provide, 7 | Inject, 8 | Injectable, 9 | Renderer, 10 | RenderViewRef, 11 | RenderElementRef 12 | } from 'angular2/core'; 13 | import { 14 | DefaultRenderView, 15 | } from 'angular2/src/core/render/view'; 16 | 17 | import {DOCUMENT} from 'angular2/src/platform/dom/dom_tokens'; 18 | import { 19 | DomRenderer, 20 | DomRenderer_ 21 | } from 'angular2/src/platform/dom/dom_renderer'; 22 | 23 | import {AnimationBuilder} from 'angular2/src/animate/animation_builder'; 24 | import {EventManager} from 'angular2/src/platform/dom/events/event_manager'; 25 | import {DomSharedStylesHost} from 'angular2/src/platform/dom/shared_styles_host'; 26 | import {DOM} from 'angular2/src/platform/dom/dom_adapter'; 27 | 28 | import {cssHyphenate} from '../helper'; 29 | 30 | function resolveInternalDomView(viewRef: RenderViewRef): DefaultRenderView { 31 | return >viewRef; 32 | } 33 | 34 | @Injectable() 35 | export class ServerDomRenderer_ extends DomRenderer_ { 36 | constructor( 37 | private eventManager: EventManager, 38 | private domSharedStylesHost: DomSharedStylesHost, 39 | private animate: AnimationBuilder, 40 | @Inject(DOCUMENT) document) { 41 | super(eventManager, domSharedStylesHost, animate, document); 42 | } 43 | 44 | setElementProperty(location: RenderElementRef, propertyName: string, propertyValue: any) { 45 | if (propertyName === 'value' || (propertyName === 'checked' && propertyValue !== false)) { 46 | let view: DefaultRenderView = resolveInternalDomView(location.renderView); 47 | let element = view.boundElements[(location).boundElementIndex]; 48 | if (DOM.nodeName(element) === 'input') { 49 | DOM.setAttribute(element, propertyName, propertyValue); 50 | return; 51 | } 52 | } else if (propertyName === 'src') { 53 | let view: DefaultRenderView = resolveInternalDomView(location.renderView); 54 | let element = view.boundElements[(location).boundElementIndex]; 55 | DOM.setAttribute(element, propertyName, propertyValue); 56 | return; 57 | } 58 | return super.setElementProperty(location, propertyName, propertyValue); 59 | } 60 | 61 | setElementStyle(location: RenderElementRef, styleName: string, styleValue: string): void { 62 | let styleNameCased = cssHyphenate(styleName); 63 | super.setElementProperty(location, styleNameCased, styleValue); 64 | } 65 | 66 | invokeElementMethod(location: RenderElementRef, methodName: string, args: any[]) { 67 | if (methodName === 'focus') { 68 | let view: DefaultRenderView = resolveInternalDomView(location.renderView); 69 | let element = view.boundElements[(location).boundElementIndex]; 70 | if (DOM.nodeName(element) === 'input') { 71 | DOM.invoke(element, 'autofocus', null); 72 | return; 73 | } 74 | 75 | } 76 | return super.invokeElementMethod(location, methodName, args); 77 | } 78 | 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /modules/universal/server/test/router_server_spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import {LocationStrategy} from 'angular2/src/router/location_strategy'; 3 | import {ServerLocationStrategy, SERVER_LOCATION_PROVIDERS} from '../src/router/server_router'; 4 | import {Component, Directive, View} from 'angular2/core'; 5 | import {ROUTER_DIRECTIVES, ROUTER_BINDINGS, RouteConfig, Router} from 'angular2/router'; 6 | 7 | /** 8 | * These tests are pretty basic, but just have something in 9 | * place that we can expand in the future 10 | */ 11 | describe('server_router', () => { 12 | 13 | var serverLocationStrategy: ServerLocationStrategy = null; 14 | 15 | beforeAll( () => { 16 | serverLocationStrategy = new ServerLocationStrategy('/'); 17 | }); 18 | afterAll( () => { 19 | serverLocationStrategy = null; 20 | }); 21 | 22 | describe('ServerLocationStrategy', () => { 23 | it('should be defined', () => { 24 | expect(serverLocationStrategy).toBeDefined(); 25 | }); 26 | 27 | describe('should have all methods defined and functional', () => { 28 | 29 | it('should have method path()', () => { 30 | spyOn(serverLocationStrategy, 'path'); 31 | serverLocationStrategy.path(); 32 | expect(serverLocationStrategy.path).toHaveBeenCalled(); 33 | }); 34 | 35 | it('should have method forward()', () => { 36 | spyOn(serverLocationStrategy, 'forward'); 37 | serverLocationStrategy.forward(); 38 | expect(serverLocationStrategy.forward).toHaveBeenCalled(); 39 | }); 40 | 41 | it('should have method back()', () => { 42 | spyOn(serverLocationStrategy, 'back'); 43 | serverLocationStrategy.back(); 44 | expect(serverLocationStrategy.back).toHaveBeenCalled(); 45 | }); 46 | 47 | it('should have method getBaseHref()', () => { 48 | spyOn(serverLocationStrategy, 'getBaseHref').and.callThrough(); 49 | var baseHref = serverLocationStrategy.getBaseHref(); 50 | expect(serverLocationStrategy.getBaseHref).toHaveBeenCalled(); 51 | expect(baseHref).toEqual('/'); 52 | }); 53 | 54 | it('should have method onPopState()', () => { 55 | spyOn(serverLocationStrategy, 'onPopState'); 56 | var fn = () => {}; 57 | serverLocationStrategy.onPopState(fn); 58 | expect(serverLocationStrategy.onPopState).toHaveBeenCalled(); 59 | expect(serverLocationStrategy.onPopState).toHaveBeenCalledWith(fn); 60 | }); 61 | 62 | it('should have method pushState()', () => { 63 | spyOn(serverLocationStrategy, 'pushState'); 64 | var opts = { 65 | state: {}, 66 | title: 'foo', 67 | url: '/bar' 68 | }; 69 | serverLocationStrategy.pushState(opts.state, opts.title, opts.url); 70 | expect(serverLocationStrategy.pushState).toHaveBeenCalled(); 71 | expect(serverLocationStrategy.pushState).toHaveBeenCalledWith(opts.state, opts.title, opts.url); 72 | }); 73 | 74 | 75 | }); 76 | 77 | }); 78 | 79 | }); 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/angular/universal.svg?branch=master)](https://travis-ci.org/angular/universal) 2 | [![npm version](https://badge.fury.io/js/angular2-universal-preview.svg)](http://badge.fury.io/js/angular2-universal-preview) 3 | [![Join the chat at https://gitter.im/angular/universal](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/universal?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Issue Stats](http://issuestats.com/github/angular/universal/badge/pr?style=flat)](http://issuestats.com/github/angular/universal) 5 | [![Issue Stats](http://issuestats.com/github/angular/universal/badge/issue?style=flat)](http://issuestats.com/github/angular/universal) 6 | 7 |

8 | 9 | Universal Angular 2 10 | 11 |

12 | 13 | # Universal Angular 2 14 | > Universal (isomorphic) JavaScript support for Angular 2 15 | 16 | # Table of Contents 17 | * [Modules](#modules) 18 | * [Universal](#universal) 19 | * [preboot.js](#prebootjs) 20 | * [Best Practices](#best-practices) 21 | * [What's in a name?](#whats-in-a-name) 22 | * [License](#license) 23 | 24 | # Modules 25 | 26 | ## [Universal](/modules/universal) 27 | > Manage your application lifecycle and serialize changes while on the server to be sent to the client 28 | 29 | ### Documentation 30 | [Design Doc](https://docs.google.com/document/d/1q6g9UlmEZDXgrkY88AJZ6MUrUxcnwhBGS0EXbVlYicY) 31 | 32 | ### Videos 33 | Full Stack Angular 2 - AngularConnect, Oct 2015 34 | [![Full Stack Angular 2](https://img.youtube.com/vi/MtoHFDfi8FM/0.jpg)](https://www.youtube.com/watch?v=MtoHFDfi8FM) 35 | 36 | Angular 2 Server Rendering - Angular U, July 2015 37 | [![Angular 2 Server Rendering](http://img.youtube.com/vi/0wvZ7gakqV4/0.jpg)](http://www.youtube.com/watch?v=0wvZ7gakqV4) 38 | 39 | ## [preboot.js](/modules/preboot) 40 | > Control server-rendered page and transfer state before client-side web app loads to the client-side-app. 41 | 42 | # Best Practices 43 | > When building Universal components in Angular 2 there are a few things to keep in mind 44 | 45 | * Know the difference between attributes and properties in relation to the DOM 46 | * Don't manipulate the `nativeElement` directly. Use the `Renderer` 47 | ```typescript 48 | constructor(element: ElementRef, renderer: Renderer) { 49 | renderer.setElementStyle(element, 'fontSize', 'x-large'); 50 | } 51 | ``` 52 | * Don't use any of the browser types provided in the global namespace such as `navigator` or `document`. Anything outside of Angular will not be detected when serializing your application into html 53 | * Keep your directives stateless as much as possible. For stateful directives you may need to provide an attribute that reflects the corresponding property with an initial string value such as `url` in `img` tag. For our native `` element the `src` attribute is reflected as the `src` property of the element type `HTMLImageElement`. 54 | 55 | # What's in a name? 56 | We believe that using the word "universal" is correct when referring to a JavaScript Application that runs in more environments than the browser. (inspired by [Universal JavaScript](https://medium.com/@mjackson/universal-javascript-4761051b7ae9)) 57 | 58 | # License 59 | [Apache-2.0](/LICENSE) 60 | -------------------------------------------------------------------------------- /modules/preboot/test/client/replay/replay_after_rerender_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {replayEvents} from '../../../src/client/replay/replay_after_rerender'; 4 | 5 | describe('replay_after_rerender', function () { 6 | describe('replayEvents()', function () { 7 | it('should do nothing and return empty array if no params', function () { 8 | let preboot = { dom: {} }; 9 | let strategy = {}; 10 | let events = []; 11 | let expected = []; 12 | let actual = replayEvents(preboot, strategy, events); 13 | expect(actual).toEqual(expected); 14 | }); 15 | 16 | it('should dispatch all events', function () { 17 | let node1 = { name: 'node1', dispatchEvent: function (evt) {} }; 18 | let node2 = { name: 'node2', dispatchEvent: function (evt) {} }; 19 | let preboot = { 20 | dom: { 21 | findClientNode: function (node) { return node; } 22 | }, 23 | log: function () {} 24 | }; 25 | let strategy = {}; 26 | let events = [ 27 | { name: 'evt1', event: { name: 'evt1' }, node: node1 }, 28 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 29 | ]; 30 | let expected = []; 31 | 32 | spyOn(node1, 'dispatchEvent'); 33 | spyOn(node2, 'dispatchEvent'); 34 | spyOn(preboot.dom, 'findClientNode').and.callThrough(); 35 | spyOn(preboot, 'log'); 36 | 37 | let actual = replayEvents(preboot, strategy, events); 38 | expect(actual).toEqual(expected); 39 | expect(node1.dispatchEvent).toHaveBeenCalledWith(events[0].event); 40 | expect(node2.dispatchEvent).toHaveBeenCalledWith(events[1].event); 41 | expect(preboot.dom.findClientNode).toHaveBeenCalledWith(node1); 42 | expect(preboot.dom.findClientNode).toHaveBeenCalledWith(node2); 43 | expect(preboot.log).toHaveBeenCalledWith(3, node1, node1, events[0].event); 44 | expect(preboot.log).toHaveBeenCalledWith(3, node2, node2, events[1].event); 45 | }); 46 | 47 | it('should dispatch one event and return the other', function () { 48 | let node1 = { name: 'node1', dispatchEvent: function (evt) {} }; 49 | let node2 = { name: 'node2', dispatchEvent: function (evt) {} }; 50 | let preboot = { 51 | dom: { 52 | findClientNode: function (node) { 53 | return node.name === 'node1' ? node : null; 54 | } 55 | }, 56 | log: function () {} 57 | }; 58 | let strategy = {}; 59 | let events = [ 60 | { name: 'evt1', event: { name: 'evt1' }, node: node1 }, 61 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 62 | ]; 63 | let expected = [ 64 | { name: 'evt2', event: { name: 'evt2' }, node: node2 } 65 | ]; 66 | 67 | spyOn(node1, 'dispatchEvent'); 68 | spyOn(node2, 'dispatchEvent'); 69 | spyOn(preboot.dom, 'findClientNode').and.callThrough(); 70 | spyOn(preboot, 'log'); 71 | 72 | let actual = replayEvents(preboot, strategy, events); 73 | expect(actual).toEqual(expected); 74 | expect(node1.dispatchEvent).toHaveBeenCalledWith(events[0].event); 75 | expect(node2.dispatchEvent).not.toHaveBeenCalled(); 76 | expect(preboot.dom.findClientNode).toHaveBeenCalledWith(node1); 77 | expect(preboot.dom.findClientNode).toHaveBeenCalledWith(node2); 78 | expect(preboot.log).toHaveBeenCalledWith(3, node1, node1, events[0].event); 79 | expect(preboot.log).toHaveBeenCalledWith(4, node2); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /modules/universal/server/src/render.ts: -------------------------------------------------------------------------------- 1 | import {bootstrap} from './platform/node'; 2 | import {Promise} from 'angular2/src/facade/async'; 3 | 4 | import { 5 | selectorRegExpFactory, 6 | arrayFlattenTree 7 | } from './helper'; 8 | import {stringifyElement} from './stringifyElement'; 9 | 10 | 11 | // import {PRIME_CACHE} from './http/server_http'; 12 | 13 | import { 14 | prebootConfigDefault, 15 | getPrebootCSS, 16 | createPrebootHTML 17 | } from './ng_preboot'; 18 | 19 | import {getClientCode} from 'preboot'; 20 | 21 | 22 | import {isBlank, isPresent} from 'angular2/src/facade/lang'; 23 | 24 | import {SharedStylesHost} from 'angular2/src/platform/dom/shared_styles_host'; 25 | 26 | import {Http} from 'angular2/http'; 27 | 28 | import {NgZone, DirectiveResolver, ComponentRef} from 'angular2/core'; 29 | 30 | export var serverDirectiveResolver = new DirectiveResolver(); 31 | 32 | export function selectorResolver(componentType: /*Type*/ any): string { 33 | return serverDirectiveResolver.resolve(componentType).selector; 34 | } 35 | 36 | 37 | export function serializeApplication(element: any, styles: string[], cache?: any): string { 38 | // serialize all style hosts 39 | let serializedStyleHosts: string = styles.length >= 1 ? '' : ''; 40 | 41 | // serialize Top Level Component 42 | let serializedCmp: string = stringifyElement(element); 43 | 44 | // serialize App Data 45 | let serializedData: string = !cache ? '' : ''+ 46 | '' 49 | ''; 50 | 51 | return serializedStyleHosts + serializedCmp + serializedData; 52 | } 53 | 54 | 55 | export function appRefSyncRender(appRef: any): string { 56 | // grab parse5 html element 57 | let element = appRef.location.nativeElement; 58 | 59 | // TODO: we need a better way to manage the style host for server/client 60 | let sharedStylesHost = appRef.injector.get(SharedStylesHost); 61 | let styles: Array = sharedStylesHost.getAllStyles(); 62 | 63 | // TODO: we need a better way to manage data serialized data for server/client 64 | // let http = appRef.injector.getOptional(Http); 65 | // let cache = isPresent(http) ? arrayFlattenTree(http._rootNode.children, []) : null; 66 | 67 | let serializedApp: string = serializeApplication(element, styles); 68 | // return stringifyElement(element); 69 | return serializedApp; 70 | } 71 | 72 | export function renderToString(AppComponent: any, serverProviders?: any): Promise { 73 | return bootstrap(AppComponent, serverProviders) 74 | .then(appRef => { 75 | let html = appRefSyncRender(appRef); 76 | appRef.dispose(); 77 | return html; 78 | // let http = appRef.injector.getOptional(Http); 79 | // // TODO: fix zone.js ensure overrideOnEventDone callback when there are no pending tasks 80 | // // ensure all xhr calls are done 81 | // return new Promise(resolve => { 82 | // let ngZone = appRef.injector.get(NgZone); 83 | // // ngZone 84 | // ngZone.overrideOnEventDone(() => { 85 | // if (isBlank(http) || isBlank(http._async) || http._async <= 0) { 86 | // let html: string = appRefSyncRender(appRef); 87 | // appRef.dispose(); 88 | // resolve(html); 89 | // } 90 | 91 | // }, true); 92 | 93 | // }); 94 | 95 | }); 96 | } 97 | 98 | 99 | export function renderToStringWithPreboot(AppComponent: any, serverProviders?: any, prebootConfig: any = {}): Promise { 100 | return renderToString(AppComponent, serverProviders) 101 | .then((html: string) => { 102 | if (typeof prebootConfig === 'boolean' && prebootConfig === false) { return html } 103 | let config = prebootConfigDefault(prebootConfig); 104 | return getClientCode(config) 105 | .then(code => html + createPrebootHTML(code, config)); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /modules/preboot/src/server/client_code_generator.ts: -------------------------------------------------------------------------------- 1 | import * as Q from 'q'; 2 | import uglify = require('gulp-uglify'); 3 | import insert = require('gulp-insert'); 4 | import rename = require('gulp-rename'); 5 | import eventStream = require('event-stream'); 6 | import buffer = require('vinyl-buffer'); 7 | import source = require('vinyl-source-stream'); 8 | import * as browserify from 'browserify'; 9 | import {normalize, listenStrategies, replayStrategies, freezeStrategies} from './normalize'; 10 | import {stringifyWithFunctions} from './utils'; 11 | import {PrebootOptions} from '../interfaces/preboot_options'; 12 | 13 | // map of input opts to client code; exposed for testing purposes 14 | export let clientCodeCache = {}; 15 | 16 | /** 17 | * We want to use the browserify ignore functionality so that any code modules 18 | * that are not being used are stubbed out. So, for example, if in the preboot 19 | * options the only listen strategy is selectors, then the event_bindings and 20 | * attributes strategies will be stubbed out (meaing the refs will be {}) 21 | */ 22 | export function ignoreUnusedStrategies(b: BrowserifyObject, bOpts: Object, strategyOpts: any[], allStrategies: Object, pathPrefix: string) { 23 | let activeStrategies = strategyOpts 24 | .filter(x => x.name) 25 | .map(x => x.name); 26 | 27 | Object.keys(allStrategies) 28 | .filter(x => activeStrategies.indexOf(x) < 0) 29 | .forEach(x => b.ignore(pathPrefix + x + '.js', bOpts)); 30 | } 31 | 32 | /** 33 | * Generate client code as a readable stream for preboot based on the input options 34 | */ 35 | export function getClientCodeStream(opts?: PrebootOptions): NodeJS.ReadableStream { 36 | opts = normalize(opts); 37 | 38 | let bOpts = { 39 | entries: [__dirname + '/../client/preboot_client.js'], 40 | standalone: 'preboot', 41 | basedir: __dirname + '/../client', 42 | browserField: false 43 | }; 44 | let b = browserify(bOpts); 45 | 46 | // ignore any strategies that are not being used 47 | ignoreUnusedStrategies(b, bOpts, opts.listen, listenStrategies, './listen/listen_by_'); 48 | ignoreUnusedStrategies(b, bOpts, opts.replay, replayStrategies, './replay/replay_after_'); 49 | 50 | if (opts.freeze) { 51 | ignoreUnusedStrategies(b, bOpts, [opts.freeze], freezeStrategies, './freeze/freeze_with_'); 52 | } 53 | 54 | // ignore other code not being used 55 | if (!opts.buffer) { b.ignore('./buffer_manager.js', bOpts); } 56 | if (!opts.debug) { b.ignore('./log.js', bOpts); } 57 | 58 | // use gulp to get the stream with the custom preboot client code 59 | let outputStream = b.bundle() 60 | .pipe(source('src/client/preboot_client.js')) 61 | .pipe(buffer()) 62 | .pipe(insert.append('\n\n;preboot.init(' + stringifyWithFunctions(opts) + ');\n\n')) 63 | .pipe(rename('preboot.js')); 64 | 65 | // uglify if the option is passed in 66 | return opts.uglify ? outputStream.pipe(uglify()) : outputStream; 67 | } 68 | 69 | /** 70 | * Generate client code as a string for preboot 71 | * based on the input options 72 | */ 73 | export function getClientCode(opts?: PrebootOptions, done?: Function) { 74 | let deferred = Q.defer(); 75 | let clientCode = ''; 76 | 77 | // check cache first 78 | let cacheKey = JSON.stringify(opts); 79 | if (clientCodeCache[cacheKey]) { 80 | return Q.when(clientCodeCache[cacheKey]); 81 | } 82 | 83 | // get the client code 84 | getClientCodeStream(opts) 85 | .pipe(eventStream.map(function(file, cb) { 86 | clientCode += file.contents; 87 | cb(null, file); 88 | })) 89 | .on('error', function(err) { 90 | if (done) { 91 | done(err); 92 | } 93 | 94 | deferred.reject(err); 95 | }) 96 | .on('end', function() { 97 | if (done) { 98 | done(null, clientCode); 99 | } 100 | 101 | clientCodeCache[cacheKey] = clientCode; 102 | deferred.resolve(clientCode); 103 | }); 104 | 105 | return deferred.promise; 106 | } 107 | -------------------------------------------------------------------------------- /modules/preboot/test/server/presets_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import presetFns from '../../src/server/presets'; 4 | 5 | /** 6 | * These tests are pretty basic, but just have something in 7 | * place that we can expand in the future 8 | */ 9 | describe('presets', function () { 10 | 11 | describe('keyPress()', function () { 12 | it('should add listen selector', function () { 13 | let opts = { listen: [] }; 14 | let expected = { 15 | listen: [ 16 | { 17 | name: 'selectors', 18 | eventsBySelector: { 19 | 'input[type="text"],textarea': ['keypress', 'keyup', 'keydown'] 20 | } 21 | }, 22 | { 23 | name: 'selectors', 24 | eventsBySelector: { 25 | 'input[type="checkbox"],input[type="radio"],select,option': ['change'] 26 | } 27 | } 28 | ] 29 | }; 30 | presetFns.keyPress(opts); 31 | expect(opts).toEqual(expected); 32 | }); 33 | }); 34 | 35 | describe('focus()', function () { 36 | it('should add listen selector', function () { 37 | let opts = { listen: [] }; 38 | let expected = { 39 | listen: [{ 40 | name: 'selectors', 41 | eventsBySelector: { 42 | 'input[type="text"],textarea': ['focusin', 'focusout', 'mousedown', 'mouseup'] 43 | }, 44 | trackFocus: true, 45 | doNotReplay: true 46 | }] 47 | }; 48 | presetFns.focus(opts); 49 | expect(opts).toEqual(expected); 50 | }); 51 | }); 52 | 53 | describe('buttonPress()', function () { 54 | it('should add listen selector', function () { 55 | let opts = { listen: [], freeze: { name: 'spinner', eventName: 'yoyo' } }; 56 | let expected = { 57 | listen: [{ 58 | name: 'selectors', 59 | preventDefault: true, 60 | eventsBySelector: { 61 | 'input[type="submit"],button': ['click'] 62 | }, 63 | dispatchEvent: opts.freeze.eventName 64 | }], 65 | freeze: { name: 'spinner', eventName: 'yoyo' } 66 | }; 67 | presetFns.buttonPress(opts); 68 | expect(opts).toEqual(expected); 69 | }); 70 | }); 71 | 72 | describe('pauseOnTyping()', function () { 73 | it('should add listen selector', function () { 74 | let opts = { listen: [], pauseEvent: 'foo', resumeEvent: 'choo' }; 75 | let expected = { 76 | listen: [ 77 | { 78 | name: 'selectors', 79 | eventsBySelector: { 80 | 'input[type="text"]': ['focus'], 81 | 'textarea': ['focus'] 82 | }, 83 | doNotReplay: true, 84 | dispatchEvent: opts.pauseEvent 85 | }, 86 | { 87 | name: 'selectors', 88 | eventsBySelector: { 89 | 'input[type="text"]': ['blur'], 90 | 'textarea': ['blur'] 91 | }, 92 | doNotReplay: true, 93 | dispatchEvent: opts.resumeEvent 94 | } 95 | ], 96 | pauseEvent: opts.pauseEvent, 97 | resumeEvent: opts.resumeEvent 98 | }; 99 | presetFns.pauseOnTyping(opts); 100 | expect(opts).toEqual(expected); 101 | }); 102 | }); 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-universal", 3 | "main": "index.js", 4 | "version": "0.0.1", 5 | "description": "Universal (isomorphic) javascript support for Angular2", 6 | "homepage": "https://github.com/angular/universal", 7 | "license": "Apache-2.0", 8 | "contributors": [ 9 | "Tobias Bosch ", 10 | "PatrickJS ", 11 | "Jeff Whelpley " 12 | ], 13 | "scripts": { 14 | "prestart": "gulp build", 15 | "build": "gulp build.typescript && gulp build.preboot", 16 | "serve": "gulp serve", 17 | "start": "gulp server", 18 | "watch": "gulp watch", 19 | "debug": "gulp debug", 20 | "clean": "gulp clean", 21 | "preboot": "gulp build.preboot", 22 | "test": "gulp karma", 23 | "ci": "gulp ci", 24 | "changelog": "gulp changelog", 25 | "e2e": "gulp protractor", 26 | "webdriver-update": "gulp protractor.update", 27 | "webdriver-start": "gulp protractor.start", 28 | "preprotractor": "gulp protractor.update", 29 | "protractor": "gulp protractor", 30 | "remove-dist": "rimraf ./dist", 31 | "remove-tsd-typing": "rimraf ./tsd_typings", 32 | "remove-angular2": "rimraf ./node_modules/angular2", 33 | "remove-angular-typings": "rimraf ./angular/modules/angular2/typings", 34 | "remove-angular-dist": "rimraf ./angular/dist", 35 | "remove-web-modules": "rimraf ./web_modules", 36 | "bower": "bower install", 37 | "web_modules": "bash ./scripts/update-ng-bundle.sh", 38 | "tsd": "tsd reinstall && tsd link", 39 | "postinstall": "npm run tsd && tsc || true && npm run remove-web-modules && npm run web_modules && npm run bower && npm run webdriver-update" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/angular/universal" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/angular/universal/issues" 47 | }, 48 | "devDependencies": { 49 | "bower": "^1.4.1", 50 | "brfs": "^1.4.0", 51 | "browser-sync": "^2.8.2", 52 | "connect-history-api-fallback": "^1.1.0", 53 | "connect-livereload": "^0.5.3", 54 | "conventional-changelog": "^0.5.0", 55 | "del": "^2.0.2", 56 | "express": "^4.13.0", 57 | "gulp-eslint": "^1.0.0", 58 | "gulp-jasmine": "^2.0.1", 59 | "gulp-livereload": "^3.8.0", 60 | "gulp-load-plugins": "^1.1.0", 61 | "gulp-node-inspector": "^0.1.0", 62 | "gulp-nodemon": "^2.0.3", 63 | "gulp-notify": "^2.2.0", 64 | "gulp-protractor": "^2.1.0", 65 | "gulp-size": "^2.0.0", 66 | "gulp-tslint": "^4.2.2", 67 | "gulp-typescript": "^2.9.0", 68 | "jasmine": "^2.3.1", 69 | "jasmine-reporters": "^2.0.7", 70 | "jasmine-spec-reporter": "^2.4.0", 71 | "karma": "^0.13.3", 72 | "karma-browserify": "^4.3.0", 73 | "karma-chrome-launcher": "^0.2.0", 74 | "karma-jasmine": "^0.3.6", 75 | "karma-phantomjs-launcher": "^0.2.1", 76 | "morgan": "^1.6.1", 77 | "nodemon": "^1.3.7", 78 | "open": "0.0.5", 79 | "opn": "^3.0.2", 80 | "phantomjs": "^1.9.17", 81 | "protractor": "^3.0.0", 82 | "rimraf": "^2.4.3", 83 | "selenium-webdriver": "^2.46.1", 84 | "serve-index": "^1.7.0", 85 | "serve-static": "^1.10.0", 86 | "tsd": "^0.6.4", 87 | "tslint": "^3.2.1", 88 | "typescript": "^1.7.3", 89 | "yargs": "^3.14.0" 90 | }, 91 | "dependencies": { 92 | "angular2": "2.0.0-beta.0", 93 | "browserify": "^11.0.0", 94 | "css": "^2.2.1", 95 | "es6-shim": "^0.33.8", 96 | "event-stream": "^3.3.1", 97 | "gulp": "^3.9.0", 98 | "gulp-insert": "^0.5.0", 99 | "gulp-rename": "^1.2.2", 100 | "gulp-uglify": "^1.2.0", 101 | "lodash": "^3.10.1", 102 | "parse5": "^1.5.0", 103 | "q": "^1.4.1", 104 | "reflect-metadata": "0.1.2", 105 | "require-dir": "^0.3.0", 106 | "run-sequence": "^1.1.2", 107 | "rxjs": "5.0.0-beta.0", 108 | "vinyl-buffer": "^1.0.0", 109 | "vinyl-source-stream": "^1.1.0", 110 | "xhr2": "^0.1.3", 111 | "zone.js": "0.5.10" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /modules/universal/client/src/ng_preload_cache.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Http, 3 | Response, 4 | Headers, 5 | RequestOptions, 6 | ResponseOptions, 7 | ConnectionBackend, 8 | XHRBackend 9 | } from 'angular2/http'; 10 | import {ObservableWrapper} from 'angular2/src/facade/async'; 11 | import { 12 | isPresent, 13 | isBlank, 14 | CONST_EXPR 15 | } from 'angular2/src/facade/lang'; 16 | 17 | import { 18 | provide, 19 | OpaqueToken, 20 | Injectable, 21 | Optional, 22 | Inject, 23 | EventEmitter 24 | } from 'angular2/core'; 25 | 26 | import { 27 | Observable 28 | } from 'rxjs'; 29 | 30 | export const PRIME_CACHE: OpaqueToken = CONST_EXPR(new OpaqueToken('primeCache')); 31 | 32 | 33 | @Injectable() 34 | export class NgPreloadCacheHttp extends Http { 35 | prime: boolean = true; 36 | constructor( 37 | protected _backend: ConnectionBackend, 38 | protected _defaultOptions: RequestOptions) { 39 | super(_backend, _defaultOptions); 40 | } 41 | 42 | preload(method) { 43 | let obs = new EventEmitter(); 44 | let newcache = (window).ngPreloadCache; 45 | if (newcache) { 46 | 47 | var preloaded = null; 48 | 49 | try { 50 | let res; 51 | preloaded = newcache.shift(); 52 | if (isPresent(preloaded)) { 53 | let body = preloaded._body; 54 | res = new ResponseOptions((Object).assign({}, preloaded, { body })); 55 | 56 | if (preloaded.headers) { 57 | res.headers = new Headers(preloaded); 58 | } 59 | preloaded = new Response(res); 60 | } 61 | } catch(e) { 62 | console.log('WAT', e) 63 | } 64 | 65 | if (preloaded) { 66 | setTimeout(() => { 67 | ObservableWrapper.callNext(obs, preloaded); 68 | // setTimeout(() => { 69 | ObservableWrapper.callComplete(obs); 70 | // }); 71 | }); 72 | return obs; 73 | } 74 | 75 | } 76 | let request = method(); 77 | // request.observer(obs); 78 | request.observer({ 79 | next(value) { 80 | ObservableWrapper.callNext(obs, value); 81 | }, 82 | throw(e) { 83 | setTimeout(() => { 84 | ObservableWrapper.callError(obs, e) 85 | }); 86 | }, 87 | return() { 88 | setTimeout(() => { 89 | ObservableWrapper.callComplete(obs) 90 | }); 91 | } 92 | }); 93 | 94 | return obs; 95 | } 96 | 97 | request(url: string, options): Observable { 98 | return this.prime ? this.preload(() => super.request(url, options)) : super.request(url, options); 99 | } 100 | 101 | get(url: string, options): Observable { 102 | return this.prime ? this.preload(() => super.get(url, options)) : super.get(url, options); 103 | } 104 | 105 | post(url: string, body: string, options): Observable { 106 | return this.prime ? this.preload(() => super.post(url, body, options)) : super.post(url, body, options); 107 | } 108 | 109 | put(url: string, body: string, options): Observable { 110 | return this.prime ? this.preload(() => super.put(url, body, options)) : super.put(url, body, options); 111 | } 112 | 113 | delete(url: string, options): Observable { 114 | return this.prime ? this.preload(() => super.delete(url, options)) : super.delete(url, options); 115 | } 116 | 117 | patch(url: string, body: string, options): Observable { 118 | return this.prime ? this.preload(() => super.patch(url, body, options)) : super.patch(url, body, options); 119 | } 120 | 121 | head(url: string, options): Observable { 122 | return this.prime ? this.preload(() => super.head(url, options)) : super.head(url, options); 123 | } 124 | } 125 | 126 | export const NG_PRELOAD_CACHE_PROVIDERS = [ 127 | provide(Http, { 128 | useFactory: (xhrBackend, requestOptions) => { 129 | return new NgPreloadCacheHttp(xhrBackend, requestOptions); 130 | }, 131 | deps: [XHRBackend, RequestOptions] 132 | }) 133 | ]; 134 | -------------------------------------------------------------------------------- /modules/universal/server/src/express/engine.ts: -------------------------------------------------------------------------------- 1 | import '../server_patch'; 2 | import * as fs from 'fs'; 3 | import {selectorRegExpFactory} from '../helper'; 4 | 5 | 6 | import { 7 | renderToString, 8 | renderToStringWithPreboot, 9 | selectorResolver 10 | } from '../render'; 11 | 12 | import { 13 | prebootScript, 14 | angularScript, 15 | bootstrapButton, 16 | bootstrapFunction, 17 | bootstrapApp, 18 | buildClientScripts 19 | } from '../ng_scripts'; 20 | 21 | import {enableProdMode} from 'angular2/core'; 22 | 23 | export interface engineOptions { 24 | App: Function; 25 | providers?: Array; 26 | preboot?: Object | any; 27 | selector?: string; 28 | serializedCmp?: string; 29 | server?: boolean; 30 | client?: boolean; 31 | enableProdMode?: boolean; 32 | } 33 | 34 | export function ng2engine(filePath: string, options: engineOptions, done: Function) { 35 | // defaults 36 | options = options || {}; 37 | options.providers = options.providers || null; 38 | 39 | // read file on disk 40 | try { 41 | fs.readFile(filePath, (err, content) => { 42 | 43 | if (err) { return done(err); } 44 | 45 | // convert to string 46 | var clientHtml: string = content.toString(); 47 | 48 | // TODO: better build scripts abstraction 49 | if (options.server === false && options.client === false) { 50 | return done(null, clientHtml); 51 | } 52 | if (options.server === false && options.client !== false) { 53 | return done(null, buildClientScripts(clientHtml, options)); 54 | } 55 | if (options.enableProdMode) { 56 | enableProdMode(); 57 | } 58 | 59 | // bootstrap and render component to string 60 | var renderPromise: any = renderToString; 61 | var args = [options.App, options.providers]; 62 | if (options.preboot) { 63 | renderPromise = renderToStringWithPreboot; 64 | args.push(options.preboot); 65 | } 66 | 67 | renderPromise(...args) 68 | .then(serializedCmp => { 69 | 70 | let selector: string = selectorResolver(options.App); 71 | 72 | // selector replacer explained here 73 | // https://gist.github.com/gdi2290/c74afd9898d2279fef9f 74 | // replace our component with serialized version 75 | let rendered: string = clientHtml.replace( 76 | // 77 | selectorRegExpFactory(selector), 78 | // {{ serializedCmp }} 79 | serializedCmp 80 | // TODO: serializedData 81 | ); 82 | 83 | done(null, buildClientScripts(rendered, options)); 84 | }) 85 | .catch(e => { 86 | console.log(e.stack); 87 | // if server fail then return client html 88 | done(null, buildClientScripts(clientHtml, options)); 89 | }); 90 | }); 91 | } catch (e) { 92 | done(e); 93 | } 94 | }; 95 | 96 | export const ng2engineWithPreboot = ng2engine; 97 | 98 | export function simpleReplace(filePath: string, options: engineOptions, done: Function) { 99 | // defaults 100 | options = options || {}; 101 | 102 | // read file on disk 103 | try { 104 | fs.readFile(filePath, (err, content) => { 105 | 106 | if (err) { return done(err); } 107 | 108 | // convert to string 109 | var clientHtml: string = content.toString(); 110 | 111 | // TODO: better build scripts abstraction 112 | if (options.server === false && options.client === false) { 113 | return done(null, clientHtml); 114 | } 115 | if (options.server === false && options.client !== false) { 116 | return done(null, buildClientScripts(clientHtml, options)); 117 | } 118 | 119 | let rendered: string = clientHtml.replace( 120 | // 121 | selectorRegExpFactory(options.selector), 122 | // {{ serializedCmp }} 123 | options.serializedCmp 124 | ); 125 | 126 | done(null, buildClientScripts(rendered, options)); 127 | }); 128 | } catch (e) { 129 | done(e); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/app/server/routes.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | var serveStatic = require('serve-static'); 4 | var historyApiFallback = require('connect-history-api-fallback'); 5 | var {Router} = require('express'); 6 | 7 | 8 | module.exports = function(ROOT) { 9 | var router = Router(); 10 | 11 | var universalPath = `${ROOT}/dist/examples/app/universal`; 12 | 13 | var {App} = require(`${universalPath}/test_page/app`); 14 | var {TodoApp} = require(`${universalPath}/todo/app`); 15 | 16 | var {provide} = require('angular2/core'); 17 | 18 | var { 19 | HTTP_PROVIDERS, 20 | SERVER_LOCATION_PROVIDERS, 21 | BASE_URL, 22 | PRIME_CACHE, 23 | queryParamsToBoolean 24 | } = require(`${ROOT}/dist/modules/universal/server/server`); 25 | // require('angular2-universal') 26 | 27 | router. 28 | route('/'). 29 | get(function ngApp(req, res) { 30 | let baseUrl = `http://localhost:3000${req.baseUrl}`; 31 | let queryParams = queryParamsToBoolean(req.query); 32 | let options = Object.assign(queryParams, { 33 | // client url for systemjs 34 | componentUrl: 'examples/app/universal/test_page/app', 35 | 36 | App: App, 37 | serverProviders: [ 38 | // HTTP_PROVIDERS, 39 | // SERVER_LOCATION_PROVIDERS, 40 | // provide(BASE_URL, {useExisting: baseUrl}), 41 | // provide(PRIME_CACHE, {useExisting: true}) 42 | ], 43 | data: {}, 44 | 45 | preboot: queryParams.preboot === false ? null : { 46 | start: true, 47 | appRoot: 'app', // selector for root element 48 | freeze: 'spinner', // show spinner w button click & freeze page 49 | replay: 'rerender', // rerender replay strategy 50 | buffer: true, // client app will write to hidden div until bootstrap complete 51 | debug: false, 52 | uglify: true, 53 | presets: ['keyPress', 'buttonPress', 'focus'] 54 | } 55 | 56 | }); 57 | 58 | res.render('app/universal/test_page/index', options); 59 | 60 | }); 61 | 62 | router. 63 | route('/examples/todo'). 64 | get(function ngTodo(req, res) { 65 | let baseUrl = `http://localhost:3000${req.baseUrl}`; 66 | let queryParams = queryParamsToBoolean(req.query); 67 | let options = Object.assign(queryParams , { 68 | // client url for systemjs 69 | componentUrl: 'examples/app/universal/todo/app', 70 | 71 | App: TodoApp, 72 | serverProviders: [ 73 | // HTTP_PROVIDERS, 74 | SERVER_LOCATION_PROVIDERS, 75 | provide(BASE_URL, {useExisting: baseUrl}), 76 | provide(PRIME_CACHE, {useExisting: true}) 77 | ], 78 | data: {}, 79 | 80 | preboot: queryParams.preboot === false ? null : { 81 | start: true, 82 | appRoot: 'app', // selector for root element 83 | freeze: 'spinner', // show spinner w button click & freeze page 84 | replay: 'rerender', // rerender replay strategy 85 | buffer: true, // client app will write to hidden div until bootstrap complete 86 | debug: false, 87 | uglify: true, 88 | presets: ['keyPress', 'buttonPress', 'focus'] 89 | } 90 | 91 | }); 92 | 93 | res.render('app/universal/todo/index', options); 94 | 95 | }); 96 | 97 | // modules 98 | router.use('/web_modules', serveStatic(`${ROOT}/web_modules`)); 99 | router.use('/bower_components', serveStatic(`${ROOT}bower_components`)); 100 | 101 | 102 | // needed for sourcemaps 103 | 104 | router.use('/src', serveStatic(ROOT + '/src')); 105 | 106 | router.use('/@reactivex/rxjs', serveStatic(`${ROOT}/node_modules/@reactivex/rxjs`)); 107 | router.use('/node_modules', serveStatic(`${ROOT}/node_modules`)); 108 | router.use('/angular2/dist', serveStatic(`${ROOT}/angular/dist/bundle`)); 109 | router.use('/examples/app', serveStatic(`${ROOT}/examples/app`)); 110 | 111 | router.use(historyApiFallback({ 112 | // verbose: true 113 | })); 114 | 115 | 116 | return router; 117 | }; 118 | -------------------------------------------------------------------------------- /modules/universal/server/src/ng_scripts.ts: -------------------------------------------------------------------------------- 1 | import {selectorRegExpFactory} from './helper'; 2 | 3 | // TODO: hard coded for now 4 | // TODO: build from preboot config 5 | // consider declarative config via directive 6 | export const prebootScript: string = ` 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | // Inject Angular for the developer 14 | export const angularScript: string = ` 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 34 | `; 35 | 36 | export const bootstrapButton: string = ` 37 |
38 | 48 | 51 |
52 | `; 53 | 54 | export function bootstrapFunction(appUrl: string): string { 55 | return ` 56 | 71 | `; 72 | }; 73 | 74 | export var bootstrapApp = ` 75 | 80 | `; 81 | 82 | export function buildScripts(scripts: any, appUrl?: string): string { 83 | // figure out what scripts to inject 84 | return (scripts === false ? '' : ( 85 | (scripts.preboot === true ? prebootScript : '') + 86 | (scripts.angular === true ? angularScript : '') + 87 | (scripts.bootstrapButton === true ? angularScript : '') + 88 | (scripts.bootstrapFunction === true ? bootstrapFunction(appUrl || '') : '') + 89 | (scripts.bootstrapApp === true ? angularScript : '') 90 | ) 91 | ); 92 | } 93 | 94 | // TODO: find better ways to configure the App initial state 95 | // to pay off this technical debt 96 | // currently checking for explicit values 97 | export function buildClientScripts(html: string, options: any): string { 98 | return html 99 | .replace( 100 | selectorRegExpFactory('preboot'), 101 | ((options.preboot === false) ? '' : prebootScript) 102 | ) 103 | .replace( 104 | selectorRegExpFactory('angular'), 105 | ((options.angular === false) ? '' : '$1' + angularScript + '$3') 106 | ) 107 | .replace( 108 | selectorRegExpFactory('bootstrap'), 109 | '$1' + 110 | ((options.bootstrap === false) ? ( 111 | bootstrapButton + 112 | bootstrapFunction(options.componentUrl) 113 | ) : ( 114 | ( 115 | (options.client === undefined || options.server === undefined) ? 116 | '' : (options.client === false) ? '' : bootstrapButton 117 | ) + 118 | bootstrapFunction(options.componentUrl) + 119 | ((options.client === false) ? '' : bootstrapApp) 120 | )) + 121 | '$3' 122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.5.0", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "removeComments": false, 9 | "noLib": false, 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": true, 13 | "listFiles": false, 14 | "outDir": "dist" 15 | }, 16 | "files": [ 17 | "tsd_typings/tsd.d.ts", 18 | "custom_typings/_custom.d.ts", 19 | "modules/index.ts", 20 | "modules/preboot/client.ts", 21 | "modules/preboot/server.ts", 22 | "modules/preboot/src/client/buffer_manager.ts", 23 | "modules/preboot/src/client/dom.ts", 24 | "modules/preboot/src/client/event_manager.ts", 25 | "modules/preboot/src/client/log.ts", 26 | "modules/preboot/src/client/preboot_client.ts", 27 | "modules/preboot/src/client/freeze/freeze_with_spinner.ts", 28 | "modules/preboot/src/client/listen/listen_by_attributes.ts", 29 | "modules/preboot/src/client/listen/listen_by_event_bindings.ts", 30 | "modules/preboot/src/client/listen/listen_by_selectors.ts", 31 | "modules/preboot/src/client/replay/replay_after_hydrate.ts", 32 | "modules/preboot/src/client/replay/replay_after_rerender.ts", 33 | "modules/preboot/src/server/client_code_generator.ts", 34 | "modules/preboot/src/server/normalize.ts", 35 | "modules/preboot/src/server/preboot_server.ts", 36 | "modules/preboot/src/server/presets.ts", 37 | "modules/preboot/src/server/utils.ts", 38 | "modules/preboot/test/preboot_karma.ts", 39 | "modules/preboot/test/server/client_code_generator_spec.ts", 40 | "modules/preboot/test/server/normalize_spec.ts", 41 | "modules/preboot/test/server/presets_spec.ts", 42 | "modules/preboot/test/server/utils_spec.ts", 43 | "modules/preboot/test/client/buffer_manager_spec.ts", 44 | "modules/preboot/test/client/dom_spec.ts", 45 | "modules/preboot/test/client/event_manager_spec.ts", 46 | "modules/preboot/test/client/log_spec.ts", 47 | "modules/preboot/test/client/freeze/freeze_with_spinner_spec.ts", 48 | "modules/preboot/test/client/listen/listen_by_attributes_spec.ts", 49 | "modules/preboot/test/client/listen/listen_by_event_bindings_spec.ts", 50 | "modules/preboot/test/client/listen/listen_by_selectors_spec.ts", 51 | "modules/preboot/test/client/replay/replay_after_hydrate_spec.ts", 52 | "modules/preboot/test/client/replay/replay_after_rerender_spec.ts", 53 | "modules/universal/server/index.ts", 54 | "modules/universal/server/server.ts", 55 | "modules/universal/server/src/platform/node.ts", 56 | "modules/universal/server/src/express/engine.ts", 57 | "modules/universal/server/src/directives/server_form.ts", 58 | "modules/universal/server/src/http/server_http.ts", 59 | "modules/universal/server/src/router/server_router.ts", 60 | "modules/universal/server/src/render/server_dom_renderer.ts", 61 | "modules/universal/server/src/ng_scripts.ts", 62 | "modules/universal/server/src/helper.ts", 63 | "modules/universal/server/src/render.ts", 64 | "modules/universal/server/src/server_patch.ts", 65 | "modules/universal/server/src/stringifyElement.ts", 66 | "modules/universal/server/test/router_server_spec.ts", 67 | "modules/universal/client/index.ts", 68 | "modules/universal/client/client.ts", 69 | "modules/universal/client/src/ng_preload_cache.ts", 70 | "examples/app/server/api.ts", 71 | "examples/app/server/routes.ts", 72 | "examples/app/server/server.ts", 73 | "examples/app/universal/test_page/app.ts", 74 | "examples/app/universal/todo/app.ts" 75 | ], 76 | "exclude": [ 77 | "modules/universal/client/test", 78 | "modules/universal/server/test", 79 | "modules/universal/node_modules", 80 | "preboot/test", 81 | "preboot/node_modules", 82 | "node_modules" 83 | ], 84 | "formatCodeOptions": { 85 | "indentSize": 2, 86 | "tabSize": 2, 87 | "newLineCharacter": "\r\n", 88 | "convertTabsToSpaces": true, 89 | "insertSpaceAfterCommaDelimiter": true, 90 | "insertSpaceAfterSemicolonInForStatements": true, 91 | "insertSpaceBeforeAndAfterBinaryOperators": true, 92 | "insertSpaceAfterKeywordsInControlFlowStatements": true, 93 | "insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, 94 | "insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 95 | "placeOpenBraceOnNewLineForFunctions": false, 96 | "placeOpenBraceOnNewLineForControlBlocks": false 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /modules/preboot/src/client/preboot_client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is the main entry point for preboot on the client side. 3 | * The primary methods are: 4 | * init() - called automatically to initialize preboot according to options 5 | * start() - when preboot should start listening to events 6 | * done() - when preboot should start replaying events 7 | */ 8 | import * as dom from './dom'; 9 | import * as eventManager from './event_manager'; 10 | import * as bufferManager from './buffer_manager'; 11 | import * as logManager from './log'; 12 | import * as freezeSpin from './freeze/freeze_with_spinner'; 13 | import {PrebootOptions} from '../interfaces/preboot_options'; 14 | 15 | // this is an impl of PrebootRef which can be passed into other client modules 16 | // so they don't have to directly ref dom or log. this used so that users can 17 | // write plugin strategies which get this object as an input param. 18 | // note that log is defined this way because browserify can blank it out. 19 | /* tslint:disable:no-empty */ 20 | let preboot = { 21 | dom: dom, 22 | log: logManager.log || function () {} 23 | }; 24 | 25 | // in each client-side module, we store state in an object so we can mock 26 | // it out during testing and easily reset it as necessary 27 | let state = { 28 | canComplete: true, // set to false if preboot paused through an event 29 | completeCalled: false, // set to true once the completion event has been raised 30 | freeze: null, // only used if freeze option is passed in 31 | opts: null, 32 | started: false 33 | }; 34 | 35 | /** 36 | * Once bootstrap has compled, we replay events, 37 | * switch buffer and then cleanup 38 | */ 39 | export function complete() { 40 | preboot.log(2, eventManager.state.events); 41 | 42 | // track that complete has been called 43 | state.completeCalled = true; 44 | 45 | // if we can't complete (i.e. preboot paused), just return right away 46 | if (!state.canComplete) { return; } 47 | 48 | // else we can complete, so get started with events 49 | let opts = state.opts; 50 | eventManager.replayEvents(preboot, opts); // replay events on client DOM 51 | if (opts.buffer) { bufferManager.switchBuffer(preboot); } // switch from server to client buffer 52 | if (opts.freeze) { state.freeze.cleanup(preboot); } // cleanup freeze divs like overlay 53 | eventManager.cleanup(preboot, opts); // cleanup event listeners 54 | } 55 | 56 | /** 57 | * Get function to run once window has loaded 58 | */ 59 | function load() { 60 | let opts = state.opts; 61 | 62 | // re-initialize dom now that we have the body 63 | dom.init({ window: window }); 64 | 65 | // make sure the app root is set 66 | dom.updateRoots(dom.getDocumentNode(opts.appRoot)); 67 | 68 | // if we are buffering, need to switch around the divs 69 | if (opts.buffer) { bufferManager.prep(preboot); } 70 | 71 | // if we could potentially freeze the UI, we need to prep (i.e. to add divs for overlay, etc.) 72 | // note: will need to alter this logic when we have more than one freeze strategy 73 | if (opts.freeze) { 74 | state.freeze = opts.freeze.name === 'spinner' ? freezeSpin : opts.freeze; 75 | state.freeze.prep(preboot, opts); 76 | } 77 | 78 | // start listening to events 79 | eventManager.startListening(preboot, opts); 80 | }; 81 | 82 | /** 83 | * Resume the completion process; if complete already called, 84 | * call it again right away 85 | */ 86 | function resume() { 87 | state.canComplete = true; 88 | 89 | if (state.completeCalled) { 90 | 91 | // using setTimeout to fix weird bug where err thrown on 92 | // serverRoot.remove() in buffer switch 93 | setTimeout(complete, 10); 94 | } 95 | } 96 | 97 | /** 98 | * Initialization is really simple. Just save the options and set 99 | * the window object. Most stuff happens with start() 100 | */ 101 | export function init(opts: PrebootOptions) { 102 | state.opts = opts; 103 | preboot.log(1, opts); 104 | dom.init({ window: window }); 105 | } 106 | 107 | /** 108 | * Start preboot by starting to record events 109 | */ 110 | export function start() { 111 | let opts = state.opts; 112 | 113 | // we can only start once, so don't do anything if called multiple times 114 | if (state.started) { return; } 115 | 116 | // initialize the window 117 | dom.init({ window: window }); 118 | 119 | // if body there, then run load handler right away, otherwise register for onLoad 120 | dom.state.body ? load() : dom.onLoad(load); 121 | 122 | // set up other handlers 123 | dom.on(opts.pauseEvent, () => state.canComplete = false); 124 | dom.on(opts.resumeEvent, resume); 125 | dom.on(opts.completeEvent, complete); 126 | } 127 | -------------------------------------------------------------------------------- /modules/preboot/test/client/buffer_manager_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {state, prep, switchBuffer} from '../../src/client/buffer_manager'; 4 | 5 | describe('buffer_manager', function () { 6 | describe('prep()', function () { 7 | it('should update the DOM roots with a new client root', function () { 8 | let clientRoot = { 9 | style: { display: 'blah' } 10 | }; 11 | let serverRoot = { 12 | cloneNode: function () { return clientRoot; }, 13 | parentNode: { 14 | insertBefore: function () {} 15 | } 16 | }; 17 | let preboot = { 18 | dom: { 19 | state: { appRoot: serverRoot }, 20 | updateRoots: function () {} 21 | } 22 | }; 23 | 24 | spyOn(serverRoot, 'cloneNode').and.callThrough(); 25 | spyOn(serverRoot.parentNode, 'insertBefore'); 26 | spyOn(preboot.dom, 'updateRoots'); 27 | 28 | prep(preboot); 29 | 30 | expect(clientRoot.style.display).toEqual('none'); 31 | expect(serverRoot.cloneNode).toHaveBeenCalled(); 32 | expect(serverRoot.parentNode.insertBefore).toHaveBeenCalledWith(clientRoot, serverRoot); 33 | expect(preboot.dom.updateRoots).toHaveBeenCalledWith(serverRoot, serverRoot, clientRoot); 34 | }); 35 | }); 36 | 37 | describe('switchBuffer()', function () { 38 | it('should switch the client and server roots', function () { 39 | let clientRoot = { 40 | style: { display: 'none' } 41 | }; 42 | let serverRoot = { 43 | nodeName: 'div' 44 | }; 45 | let preboot = { 46 | dom: { 47 | state: { clientRoot: clientRoot, serverRoot: serverRoot }, 48 | removeNode: function () {}, 49 | updateRoots: function () {} 50 | } 51 | }; 52 | 53 | spyOn(preboot.dom, 'removeNode'); 54 | spyOn(preboot.dom, 'updateRoots'); 55 | state.switched = false; 56 | 57 | switchBuffer(preboot); 58 | 59 | expect(clientRoot.style.display).toEqual('block'); 60 | expect(preboot.dom.removeNode).toHaveBeenCalledWith(serverRoot); 61 | expect(preboot.dom.updateRoots).toHaveBeenCalledWith(clientRoot, null, clientRoot); 62 | }); 63 | 64 | it('should not switch because already switched', function () { 65 | let clientRoot = { 66 | style: { display: 'none' } 67 | }; 68 | let serverRoot = { 69 | nodeName: 'div' 70 | }; 71 | let preboot = { 72 | dom: { 73 | state: { clientRoot: clientRoot, serverRoot: serverRoot }, 74 | removeNode: function () {}, 75 | updateRoots: function () {} 76 | } 77 | }; 78 | 79 | spyOn(preboot.dom, 'removeNode'); 80 | spyOn(preboot.dom, 'updateRoots'); 81 | state.switched = true; 82 | 83 | switchBuffer(preboot); 84 | 85 | expect(clientRoot.style.display).toEqual('none'); 86 | expect(preboot.dom.removeNode).not.toHaveBeenCalled(); 87 | expect(preboot.dom.updateRoots).not.toHaveBeenCalled(); 88 | }); 89 | 90 | it('should not remove server root because it is the body', function () { 91 | let clientRoot = { 92 | style: { display: 'none' } 93 | }; 94 | let serverRoot = { 95 | nodeName: 'BODY' 96 | }; 97 | let preboot = { 98 | dom: { 99 | state: { clientRoot: clientRoot, serverRoot: serverRoot }, 100 | removeNode: function () {}, 101 | updateRoots: function () {} 102 | } 103 | }; 104 | 105 | spyOn(preboot.dom, 'removeNode'); 106 | spyOn(preboot.dom, 'updateRoots'); 107 | state.switched = false; 108 | 109 | switchBuffer(preboot); 110 | 111 | expect(clientRoot.style.display).toEqual('block'); 112 | expect(preboot.dom.removeNode).not.toHaveBeenCalled(); 113 | expect(preboot.dom.updateRoots).toHaveBeenCalledWith(clientRoot, null, clientRoot); 114 | }); 115 | 116 | it('should not remove server root because it is the body', function () { 117 | let clientRoot = { 118 | style: { display: 'none' }, 119 | nodeName: 'DIV' 120 | }; 121 | let preboot = { 122 | dom: { 123 | state: { clientRoot: clientRoot, serverRoot: clientRoot }, 124 | removeNode: function () {}, 125 | updateRoots: function () {} 126 | } 127 | }; 128 | 129 | spyOn(preboot.dom, 'removeNode'); 130 | spyOn(preboot.dom, 'updateRoots'); 131 | state.switched = false; 132 | 133 | switchBuffer(preboot); 134 | 135 | expect(clientRoot.style.display).toEqual('block'); 136 | expect(preboot.dom.removeNode).not.toHaveBeenCalled(); 137 | expect(preboot.dom.updateRoots).toHaveBeenCalledWith(clientRoot, null, clientRoot); 138 | }); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /modules/preboot/README.md: -------------------------------------------------------------------------------- 1 | # preboot 2 | 3 | Control server-rendered page before client-side web app loads. 4 | 5 | **NOTE**: In the process of doing some major refactoring to this library. 6 | It works and you can try it out, but just be aware that there will be major 7 | changes coming soon. 8 | 9 | ## Key Features 10 | 11 | 1. Record and play back events 12 | 1. Respond immediately to events 13 | 1. Maintain focus even page is re-rendered 14 | 1. Buffer client-side re-rendering for smoother transition 15 | 1. Freeze page until bootstrap complete if user clicks button 16 | 17 | ## Installation 18 | 19 | This is a server-side library that generates client-side code. 20 | To use this library, you would first install it through npm: 21 | 22 | ``` 23 | npm install preboot 24 | ``` 25 | 26 | Then in your server-side code you would do something like this: 27 | 28 | ``` 29 | var preboot = require('preboot'); 30 | var prebootOptions = {}; // see options section below 31 | var clientCode = preboot(prebootOptions); 32 | ``` 33 | 34 | You then inject clientCode into the HEAD section of your server-side template. 35 | We want preboot to ONLY start recording once the web app root exists in the DOM. We are 36 | still playing with the best way to do this (NOTE: we have tried onLoad and 37 | it does not work because the callback does not get executed quickly enough). 38 | For now, try putting the following 39 | `preboot.start()` call immediately after your web app root in your server side template: 40 | 41 | ``` 42 | 43 | 44 | 45 | 48 | ``` 49 | 50 | Finally, once your client-side web app is "alive" it has to tell preboot that it is OK 51 | to replay events. 52 | 53 | ``` 54 | preboot.done(); 55 | ``` 56 | 57 | ## Examples 58 | 59 | Server-side integrations: 60 | 61 | * [Express](docs/examples.md#express) 62 | * [Hapi](docs/examples.md#hapi) 63 | * [Gulp](docs/examples.md#gulp) 64 | 65 | Client-side integrations: 66 | 67 | * [Angular 1.x](docs/examples.md#angular-1) 68 | * [Angular 2](docs/examples.md#angular-2) 69 | * [React](docs/examples.md#react) 70 | * [Ember](docs/examples.md#ember) 71 | 72 | Custom strategies: 73 | 74 | * [Listening for events](docs/examples.md#listen-strategy) 75 | * [Replaying events](docs/examples.md#replay-strategy) 76 | * [Freezing screen](docs/examples.md#freeze-strategy) 77 | 78 | ## Options 79 | 80 | There are 5 different types of options that can be passed into preboot: 81 | 82 | **1. Selectors** 83 | 84 | * `appRoot` - A selector that can be used to find the root element for the view (default is 'body') 85 | 86 | **2. Strategies** 87 | 88 | These can either be string values if you want to use a pre-built strategy that comes with the framework 89 | or you can implement your own strategy and pass it in here as a function or object. 90 | 91 | * `listen` - How preboot listens for events. See [Listen Strategies](docs/strategies.md#listen-strategies) below for more details. 92 | * `replay` - How preboot replays captured events on client view. See [Replay Strategies](docs/strategies.md#replay-strategies) below for more details. 93 | * `freeze` - How preboot freezes the screen when certain events occur. See [Freeze Strategies](docs/strategies.md#freeze-strategies) below for more details. 94 | 95 | **3. Flags** 96 | 97 | All flags flase by default. 98 | 99 | * `focus` - If true, will track and maintain focus even if page re-rendered 100 | * `buffer` - If true, client will write to a hidden div which is only displayed after bootstrap complete 101 | * `keyPress` - If true, all keystrokes in a textbox or textarea will be transferred from the server 102 | view to the client view 103 | * `buttonPress` - If true, button presses will be recorded and the UI will freeze until bootstrap complete 104 | * `pauseOnTyping` - If true, the preboot will not complete until user focus out of text input elements 105 | * `doNotReplay` - If true, none of the events recorded will be replayed 106 | 107 | **4. Workflow Events** 108 | 109 | These are the names of global events that can affect the preboot workflow: 110 | 111 | * `pauseEvent` - When this is raised, preboot will delay the play back of recorded events (default 'PrebootPause') 112 | * `resumeEvent` - When this is raised, preboot will resume the playback of events (default 'PrebootResume') 113 | 114 | **5. Build Params** 115 | 116 | * `uglify` - You can always uglify the output of the client code stream yourself, but if you set this 117 | option to true preboot will do it for you. 118 | 119 | ## Play 120 | 121 | If you want to play with this library you can clone it locally: 122 | 123 | ``` 124 | git clone git@github.com:jeffwhelpley/preboot.git 125 | cd preboot 126 | gulp build 127 | gulp play 128 | ``` 129 | 130 | Open your browser to http://localhost:3000. Make modifications to the options in build/task.build.js 131 | to see how preboot can be changed. 132 | 133 | ## Contributors 134 | 135 | We would welcome any and all contributions. Please see the [Contributors Guide](docs/contributors.md). 136 | -------------------------------------------------------------------------------- /examples/app/universal/todo/app.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // import {bootstrap} from '../../angular2_client/bootstrap-defer'; 3 | import { 4 | ViewEncapsulation, 5 | Component, 6 | View, 7 | Directive, 8 | ElementRef, 9 | bind, 10 | Inject 11 | } from 'angular2/core'; 12 | 13 | import { 14 | bootstrap 15 | } from 'angular2/bootstrap'; 16 | 17 | import { 18 | COMMON_DIRECTIVES 19 | } from 'angular2/common'; 20 | 21 | import {ROUTER_PROVIDERS, ROUTER_DIRECTIVES} from 'angular2/router'; 22 | 23 | import {Http, HTTP_PROVIDERS} from 'angular2/http'; 24 | import { 25 | NG_PRELOAD_CACHE_PROVIDERS, 26 | PRIME_CACHE 27 | } from '../../../../modules/universal/client/client'; 28 | 29 | 30 | import {Store, Todo, TodoFactory} from './services/TodoStore'; 31 | 32 | @Component({ 33 | selector: 'app', 34 | providers: [ Store, TodoFactory ], 35 | encapsulation: ViewEncapsulation.None, 36 | directives: [ROUTER_DIRECTIVES], 37 | styles: [], 38 | template: ` 39 |
40 | 41 | 51 | 52 |
53 | 58 | 59 | 60 |
    61 | 62 |
  • 66 | 67 |
    69 | 70 | 74 | 75 | 76 | 77 | 78 |
    79 | 80 |
    81 | 82 | 87 | 88 |
    89 | 90 |
  • 91 |
92 |
93 | 94 | 124 | 125 |
126 | ` 127 | }) 128 | export class TodoApp { 129 | todoEdit: Todo = null; 130 | selected: number = 0; 131 | constructor(public todoStore: Store, public factory: TodoFactory) { 132 | } 133 | 134 | onInit() { 135 | this.addTodo('Universal JavaScript'); 136 | this.addTodo('Run Angular 2 in Web Workers'); 137 | this.addTodo('Upgrade the web'); 138 | this.addTodo('Release Angular 2'); 139 | } 140 | 141 | enterTodo($event, inputElement) { 142 | if (!inputElement.value) { return; } 143 | if ($event.which !== 13) { return; } 144 | this.addTodo(inputElement.value); 145 | inputElement.value = ''; 146 | } 147 | 148 | editTodo(todo: Todo) { 149 | this.todoEdit = todo; 150 | } 151 | 152 | doneEditing($event, todo: Todo) { 153 | var which = $event.which; 154 | var target = $event.target; 155 | 156 | if (which === 13) { 157 | todo.title = target.value; 158 | this.todoEdit = null; 159 | } else if (which === 27) { 160 | this.todoEdit = null; 161 | target.value = todo.title; 162 | } 163 | 164 | } 165 | 166 | addTodo(newTitle: string) { 167 | this.todoStore.add(this.factory.create(newTitle, false)); 168 | } 169 | 170 | completeMe(todo: Todo) { 171 | todo.completed = !todo.completed; 172 | } 173 | 174 | deleteMe(todo: Todo) { 175 | this.todoStore.remove(todo); 176 | } 177 | 178 | toggleAll($event) { 179 | var isComplete = $event.target.checked; 180 | this.todoStore.list.forEach((todo: Todo) => todo.completed = isComplete); 181 | } 182 | 183 | clearCompleted() { 184 | this.todoStore.removeBy(todo => todo.completed); 185 | } 186 | 187 | pluralize(count, word) { 188 | return `word${count === 1 ? '' : 's'}`; 189 | } 190 | 191 | remainingCount() { 192 | return this.todoStore.list.filter((todo: Todo) => !todo.completed).length; 193 | } 194 | } 195 | 196 | 197 | 198 | export function main() { 199 | return bootstrap(TodoApp, [ 200 | ROUTER_PROVIDERS, 201 | HTTP_PROVIDERS, 202 | NG_PRELOAD_CACHE_PROVIDERS, 203 | bind(PRIME_CACHE).toValue(true) 204 | ]); 205 | } 206 | -------------------------------------------------------------------------------- /examples/app/universal/test_page/app.ts: -------------------------------------------------------------------------------- 1 | /// 2 | console.time('angular2/core in client'); 3 | import * as angular from 'angular2/core'; 4 | console.timeEnd('angular2/core in client'); 5 | 6 | import { 7 | Component, 8 | View, 9 | ViewEncapsulation, 10 | bind 11 | } from 'angular2/core'; 12 | 13 | import {bootstrap} from 'angular2/platform/browser'; 14 | 15 | // import { 16 | // Http, 17 | // HTTP_PROVIDERS 18 | // } from 'angular2/http'; 19 | 20 | // import { 21 | // NG_PRELOAD_CACHE_PROVIDERS, 22 | // PRIME_CACHE 23 | // } from '../../../../modules/universal/client/client'; 24 | 25 | 26 | 27 | function transformData(data) { 28 | if (data.hasOwnProperty('created_at')) { 29 | data.created_at = new Date(data.created_at); 30 | } 31 | return data; 32 | } 33 | 34 | @Component({ 35 | selector: 'app', 36 | providers: [], 37 | directives: [], 38 | styles: [` 39 | #intro { 40 | background-color: red; 41 | } 42 | `], 43 | template: ` 44 |

Hello Server Renderer

45 |

test binding {{ value }}

46 | {{ value }} 47 | {{ value }} 48 | 49 | 50 |
51 | 52 | 53 |
54 | 55 |
56 |
// App.testing()
 57 | {{ testing() | json }}
58 |
// App.clickingTest()
 59 | {{ buttonTest | json }}
60 |
61 |
62 | 68 | {{ value }} 69 |
70 |
71 | 72 |
73 | 74 |
75 | 76 |
77 |
78 | NgIf true 79 |
80 | 81 |
    82 |
  • 83 | 87 |
    {{ item | json }}
    88 |
  • 89 |
90 | 91 |
92 | 93 | 94 |
95 | 96 | 97 | 98 |

99 | Problem with default component state and stateful DOM 100 |
101 | 102 | {{ testingInput }} 103 |

104 | 105 | 106 | ` 107 | }) 108 | export class App { 109 | static queries = { 110 | todos: '/api/todos' 111 | }; 112 | 113 | value: string = 'value8'; 114 | items: Array = []; 115 | toggle: boolean = true; 116 | itemCount: number = 0; 117 | buttonTest: string = ''; 118 | testingInput: string = 'default state on component'; 119 | 120 | // todosObs1$ = this.http.get(App.queries.todos) 121 | // .filter(res => res.status >= 200 && res.status < 300) 122 | // .map(res => res.json()) 123 | // .map(data => transformData(data)); // ensure correct data prop types 124 | // todosObs2$ = this.http.get(App.queries.todos) 125 | // .filter(res => res.status >= 200 && res.status < 300) 126 | // .map(res => res.json()) 127 | // .map(data => transformData(data)); // ensure correct data prop types 128 | // todosObs3$ = this.http.get(App.queries.todos) 129 | // .map(res => res.json()) 130 | // .map(data => transformData(data)); 131 | 132 | constructor(/*private http: Http*/) { 133 | 134 | } 135 | 136 | onInit() { 137 | // this.addItem(); 138 | // this.addItem(); 139 | // this.addItem(); 140 | 141 | // this.todosObs1$.subscribe( 142 | // // onValue 143 | // todos => { 144 | // todos.map(todo => this.addItem(todo)); 145 | // this.anotherAjaxCall(); 146 | // }, 147 | // // onError 148 | // err => { 149 | // console.error('err', err); 150 | // throw err; 151 | // }, 152 | // // onComplete 153 | // () => { 154 | // console.log('complete request1'); 155 | // }); 156 | 157 | // this.todosObs2$.subscribe( 158 | // // onValue 159 | // todos => { 160 | // console.log('another call 2', todos); 161 | // todos.map(todo => this.addItem(todo)); 162 | // // this.anotherAjaxCall(); 163 | // }, 164 | // // onError 165 | // err => { 166 | // console.error('err', err); 167 | // throw err; 168 | // }, 169 | // // onComplete 170 | // () => { 171 | // console.log('complete request2'); 172 | // }); 173 | 174 | } 175 | anotherAjaxCall() { 176 | // this.todosObs3$.subscribe( 177 | // todos => { 178 | // console.log('anotherAjaxCall data 3', todos); 179 | // }, 180 | // err => { 181 | // console.log('anotherAjaxCall err') 182 | // }, 183 | // () => { 184 | // console.log('anotherAjaxCall complete ajax') 185 | // }); 186 | } 187 | 188 | log(value) { 189 | console.log('log:', value); 190 | return value; 191 | } 192 | 193 | toggleNgIf() { 194 | this.toggle = !this.toggle; 195 | } 196 | 197 | testing() { 198 | return 'testing' + 5; 199 | } 200 | 201 | clickingTest() { 202 | this.buttonTest = `click ${ this.testing() } ${ ~~(Math.random() * 20) }`; 203 | console.log(this.buttonTest); 204 | } 205 | 206 | addItem(value?: any) { 207 | if (value) { 208 | return this.items.push(value); 209 | } 210 | let defaultItem = { 211 | value: `item ${ this.itemCount++ }`, 212 | completed: true, 213 | created_at: new Date() 214 | }; 215 | return this.items.push(defaultItem); 216 | } 217 | 218 | 219 | removeItem() { 220 | this.items.pop(); 221 | } 222 | 223 | } 224 | 225 | 226 | export function main() { 227 | return bootstrap(App, [ 228 | // HTTP_PROVIDERS, 229 | // NG_PRELOAD_CACHE_PROVIDERS, 230 | // bind(PRIME_CACHE).toValue(true) 231 | ]); 232 | } 233 | -------------------------------------------------------------------------------- /modules/universal/server/src/platform/node.ts: -------------------------------------------------------------------------------- 1 | import * as http from 'http'; 2 | import * as url from 'url'; 3 | 4 | // Facade 5 | import {Type, isPresent, CONST_EXPR} from 'angular2/src/facade/lang'; 6 | import {Promise, PromiseWrapper, PromiseCompleter} from 'angular2/src/facade/promise'; 7 | 8 | // Compiler 9 | import {COMPILER_PROVIDERS, XHR} from 'angular2/compiler'; 10 | 11 | // Animate 12 | import {BrowserDetails} from 'angular2/src/animate/browser_details'; 13 | import {AnimationBuilder} from 'angular2/src/animate/animation_builder'; 14 | 15 | // Core 16 | import {Testability} from 'angular2/src/core/testability/testability'; 17 | import {ReflectionCapabilities} from 'angular2/src/core/reflection/reflection_capabilities'; 18 | import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver'; 19 | import {APP_COMPONENT} from 'angular2/src/core/application_tokens'; 20 | import { 21 | provide, 22 | Provider, 23 | PLATFORM_INITIALIZER, 24 | PLATFORM_COMMON_PROVIDERS, 25 | PLATFORM_DIRECTIVES, 26 | PLATFORM_PIPES, 27 | APPLICATION_COMMON_PROVIDERS, 28 | ComponentRef, 29 | platform, 30 | reflector, 31 | ExceptionHandler, 32 | Renderer 33 | } from 'angular2/core'; 34 | 35 | // Common 36 | import {COMMON_DIRECTIVES, COMMON_PIPES, FORM_PROVIDERS} from 'angular2/common'; 37 | 38 | // Platform 39 | import {ELEMENT_PROBE_BINDINGS,ELEMENT_PROBE_PROVIDERS,} from 'angular2/platform/common_dom'; 40 | import {Parse5DomAdapter} from 'angular2/src/platform/server/parse5_adapter'; 41 | Parse5DomAdapter.makeCurrent(); // ensure Parse5DomAdapter is used 42 | // Platform.Dom 43 | import {DOM} from 'angular2/src/platform/dom/dom_adapter'; 44 | // import {DomRenderer} from 'angular2/src/platform/dom/dom_renderer'; 45 | import {EventManager, EVENT_MANAGER_PLUGINS} from 'angular2/src/platform/dom/events/event_manager'; 46 | import {DomEventsPlugin} from 'angular2/src/platform/dom/events/dom_events'; 47 | import {KeyEventsPlugin} from 'angular2/src/platform/dom/events/key_events'; 48 | import {HammerGesturesPlugin} from 'angular2/src/platform/dom/events/hammer_gestures'; 49 | import {DomSharedStylesHost, SharedStylesHost} from 'angular2/src/platform/dom/shared_styles_host'; 50 | import {DOCUMENT} from 'angular2/src/platform/dom/dom_tokens'; 51 | import {DomRenderer} from 'angular2/src/platform/dom/dom_renderer'; 52 | 53 | import {ServerDomRenderer_} from '../render/server_dom_renderer'; 54 | 55 | export function initNodeAdapter() { 56 | Parse5DomAdapter.makeCurrent(); 57 | } 58 | 59 | export class NodeXHRImpl extends XHR { 60 | get(templateUrl: string): Promise { 61 | let completer: PromiseCompleter = PromiseWrapper.completer(), 62 | parsedUrl = url.parse(templateUrl); 63 | 64 | http.get(templateUrl, (res) => { 65 | res.setEncoding('utf8'); 66 | 67 | // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) 68 | var status = res.statusCode === 1223 ? 204 : res.statusCode; 69 | 70 | if (200 <= status && status <= 300) { 71 | let data = ''; 72 | 73 | res.on('data', (chunk) => { 74 | data += chunk; 75 | }); 76 | res.on('end', () => { 77 | completer.resolve(data); 78 | }); 79 | } 80 | else { 81 | completer.reject(`Failed to load ${templateUrl}`, null); 82 | } 83 | 84 | // consume response body 85 | res.resume(); 86 | }).on('error', (e) => { 87 | completer.reject(`Failed to load ${templateUrl}`, null); 88 | }); 89 | 90 | return completer.promise; 91 | } 92 | } 93 | 94 | export const NODE_PROVIDERS: Array = CONST_EXPR([ 95 | ...PLATFORM_COMMON_PROVIDERS, 96 | new Provider(PLATFORM_INITIALIZER, {useValue: initNodeAdapter, multi: true}), 97 | ]); 98 | 99 | function _exceptionHandler(): ExceptionHandler { 100 | return new ExceptionHandler(DOM, false); 101 | } 102 | 103 | export const NODE_APP_COMMON_PROVIDERS: Array = CONST_EXPR([ 104 | ...APPLICATION_COMMON_PROVIDERS, 105 | ...FORM_PROVIDERS, 106 | new Provider(PLATFORM_PIPES, {useValue: COMMON_PIPES, multi: true}), 107 | new Provider(PLATFORM_DIRECTIVES, {useValue: COMMON_DIRECTIVES, multi: true}), 108 | new Provider(ExceptionHandler, {useFactory: _exceptionHandler, deps: []}), 109 | new Provider(DOCUMENT, { 110 | useFactory: (appComponentType, directiveResolver) => { 111 | // TODO(gdi2290): determine a better for document on the server 112 | let selector = directiveResolver.resolve(appComponentType).selector; 113 | let serverDocument = DOM.createHtmlDocument(); 114 | let el = DOM.createElement(selector); 115 | DOM.appendChild(serverDocument.body, el); 116 | return serverDocument; 117 | }, 118 | deps: [APP_COMPONENT, DirectiveResolver] 119 | }), 120 | new Provider(EVENT_MANAGER_PLUGINS, {useClass: DomEventsPlugin, multi: true}), 121 | new Provider(EVENT_MANAGER_PLUGINS, {useClass: KeyEventsPlugin, multi: true}), 122 | new Provider(EVENT_MANAGER_PLUGINS, {useClass: HammerGesturesPlugin, multi: true}), 123 | new Provider(DomRenderer, {useClass: ServerDomRenderer_}), 124 | new Provider(Renderer, {useExisting: DomRenderer}), 125 | new Provider(SharedStylesHost, {useExisting: DomSharedStylesHost}), 126 | DomSharedStylesHost, 127 | Testability, 128 | BrowserDetails, 129 | AnimationBuilder, 130 | EventManager 131 | ]); 132 | 133 | /** 134 | * An array of providers that should be passed into `application()` when bootstrapping a component. 135 | */ 136 | export const NODE_APP_PROVIDERS: Array = CONST_EXPR([ 137 | ...NODE_APP_COMMON_PROVIDERS, 138 | ...COMPILER_PROVIDERS, 139 | new Provider(XHR, {useClass: NodeXHRImpl}), 140 | ]); 141 | 142 | /** 143 | * 144 | */ 145 | export function bootstrap( 146 | appComponentType: Type, 147 | customAppProviders: Array = null, 148 | customComponentProviders: Array = null): Promise { 149 | 150 | reflector.reflectionCapabilities = new ReflectionCapabilities(); 151 | 152 | let appProviders: Array = [ 153 | provide(APP_COMPONENT, {useValue: appComponentType}), 154 | ...NODE_APP_PROVIDERS, 155 | ...(isPresent(customAppProviders) ? customAppProviders : []) 156 | ]; 157 | 158 | let componentProviders: Array = [ 159 | ...(isPresent(customComponentProviders) ? customComponentProviders : []) 160 | ]; 161 | 162 | return platform(NODE_PROVIDERS) 163 | .application(appProviders) 164 | .bootstrap(appComponentType, componentProviders); 165 | } 166 | -------------------------------------------------------------------------------- /modules/preboot/test/client/event_manager_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as eventManager from '../../src/client/event_manager'; 4 | 5 | describe('event_manager', function () { 6 | describe('getEventHandler()', function () { 7 | it('should do nothing if not listening', function () { 8 | let preboot = { dom: {} }; 9 | let strategy = {}; 10 | let node = {}; 11 | let eventName = 'click'; 12 | let event = {}; 13 | 14 | eventManager.state.listening = false; 15 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 16 | }); 17 | 18 | it('should call preventDefault', function () { 19 | let preboot = { dom: {} }; 20 | let strategy = { preventDefault: true }; 21 | let node = {}; 22 | let eventName = 'click'; 23 | let event = { preventDefault: function () {} }; 24 | 25 | spyOn(event, 'preventDefault'); 26 | eventManager.state.listening = true; 27 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 28 | expect(event.preventDefault).toHaveBeenCalled(); 29 | }); 30 | 31 | it('should dispatch global event', function () { 32 | let preboot = { 33 | dom: { 34 | dispatchGlobalEvent: function () {} 35 | } 36 | }; 37 | let strategy = { dispatchEvent: 'yo yo yo' }; 38 | let node = {}; 39 | let eventName = 'click'; 40 | let event = {}; 41 | 42 | spyOn(preboot.dom, 'dispatchGlobalEvent'); 43 | eventManager.state.listening = true; 44 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 45 | expect(preboot.dom.dispatchGlobalEvent).toHaveBeenCalledWith(strategy.dispatchEvent); 46 | }); 47 | 48 | it('should call action', function () { 49 | let preboot = { dom: {} }; 50 | let strategy = { action: function () {} }; 51 | let node = {}; 52 | let eventName = 'click'; 53 | let event = {}; 54 | 55 | spyOn(strategy, 'action'); 56 | eventManager.state.listening = true; 57 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 58 | expect(strategy.action).toHaveBeenCalledWith(preboot, node, event); 59 | }); 60 | 61 | it('should track focus', function () { 62 | let preboot = { dom: {}, activeNode: null }; 63 | let strategy = { trackFocus: true }; 64 | let node = {}; 65 | let eventName = 'focusin'; 66 | let event = { type: 'focusin', target: { name: 'foo' }}; 67 | 68 | eventManager.state.listening = true; 69 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 70 | expect(preboot.activeNode).toEqual(event.target); 71 | }); 72 | 73 | it('should add to events', function () { 74 | let preboot = { dom: {}, time: (new Date()).getTime() }; 75 | let strategy = {}; 76 | let node = {}; 77 | let eventName = 'click'; 78 | let event = { type: 'focusin', target: { name: 'foo' }}; 79 | 80 | eventManager.state.listening = true; 81 | eventManager.state.events = []; 82 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 83 | expect(eventManager.state.events).toEqual([{ 84 | node: node, 85 | event: event, 86 | name: eventName, 87 | time: preboot.time 88 | }]); 89 | }); 90 | 91 | it('should not add events if doNotReplay', function () { 92 | let preboot = { dom: {}, time: (new Date()).getTime() }; 93 | let strategy = { doNotReplay: true }; 94 | let node = {}; 95 | let eventName = 'click'; 96 | let event = { type: 'focusin', target: { name: 'foo' }}; 97 | 98 | eventManager.state.listening = true; 99 | eventManager.state.events = []; 100 | eventManager.getEventHandler(preboot, strategy, node, eventName)(event); 101 | expect(eventManager.state.events).toEqual([]); 102 | }); 103 | }); 104 | 105 | describe('addEventListeners()', function () { 106 | it('should add nodeEvents to listeners', function () { 107 | let preboot = { dom: {} }; 108 | let nodeEvent1 = { node: { name: 'zoo', addEventListener: function () {} }, eventName: 'foo' }; 109 | let nodeEvent2 = { node: { name: 'shoo', addEventListener: function () {} }, eventName: 'moo' }; 110 | let nodeEvents = [nodeEvent1, nodeEvent2]; 111 | let strategy = {}; 112 | 113 | spyOn(nodeEvent1.node, 'addEventListener'); 114 | spyOn(nodeEvent2.node, 'addEventListener'); 115 | eventManager.state.eventListeners = []; 116 | eventManager.addEventListeners(preboot, nodeEvents, strategy); 117 | expect(nodeEvent1.node.addEventListener).toHaveBeenCalled(); 118 | expect(nodeEvent2.node.addEventListener).toHaveBeenCalled(); 119 | expect(eventManager.state.eventListeners.length).toEqual(2); 120 | expect(eventManager.state.eventListeners[0].name).toEqual(nodeEvent1.eventName); 121 | }); 122 | }); 123 | 124 | describe('startListening()', function () { 125 | it('should set the listening state', function () { 126 | let preboot = { dom: {} }; 127 | let opts = { listen: [] }; 128 | 129 | eventManager.state.listening = false; 130 | eventManager.startListening(preboot, opts); 131 | expect(eventManager.state.listening).toEqual(true); 132 | }); 133 | }); 134 | 135 | describe('replayEvents()', function () { 136 | it('should set listening to false', function () { 137 | let preboot = { dom: {}, log: function () {} }; 138 | let opts = { replay: [] }; 139 | let evts = [{ foo: 'choo' }]; 140 | 141 | spyOn(preboot, 'log'); 142 | eventManager.state.listening = true; 143 | eventManager.state.events = evts; 144 | eventManager.replayEvents(preboot, opts); 145 | expect(eventManager.state.listening).toEqual(false); 146 | expect(preboot.log).toHaveBeenCalledWith(5, evts); 147 | }); 148 | }); 149 | 150 | describe('cleanup()', function () { 151 | it('should set events to empty array', function () { 152 | let preboot = { dom: {} }; 153 | let opts = {}; 154 | 155 | eventManager.state.eventListeners = []; 156 | eventManager.state.events = [{ foo: 'moo' }]; 157 | eventManager.cleanup(preboot, opts); 158 | expect(eventManager.state.events).toEqual([]); 159 | }); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /modules/preboot/src/client/event_manager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module cooridinates all preboot events on the client side 3 | */ 4 | import {PrebootRef} from '../interfaces/preboot_ref'; 5 | import {PrebootOptions} from '../interfaces/preboot_options'; 6 | import {ListenStrategy} from '../interfaces/strategy'; 7 | import {Element} from '../interfaces/element'; 8 | import {DomEvent, NodeEvent} from '../interfaces/event'; 9 | 10 | // import all the listen and replay strategies here 11 | // note: these will get filtered out by browserify at build time 12 | import * as listenAttr from './listen/listen_by_attributes'; 13 | import * as listenEvt from './listen/listen_by_event_bindings'; 14 | import * as listenSelect from './listen/listen_by_selectors'; 15 | import * as replayHydrate from './replay/replay_after_hydrate'; 16 | import * as replayRerender from './replay/replay_after_rerender'; 17 | 18 | const caretPositionEvents = ['keyup', 'keydown', 'focusin', 'mouseup', 'mousedown']; 19 | const caretPositionNodes = ['INPUT', 'TEXTAREA']; 20 | 21 | // export state for testing purposes 22 | export let state = { 23 | eventListeners: [], 24 | events: [], 25 | listening: false 26 | }; 27 | 28 | export let strategies = { 29 | listen: { 30 | 'attributes': listenAttr, 31 | 'event_bindings': listenEvt, 32 | 'selectors': listenSelect 33 | }, 34 | replay: { 35 | 'hydrate': replayHydrate, 36 | 'rerender': replayRerender 37 | } 38 | }; 39 | 40 | /** 41 | * For a given node, add an event listener based on the given attribute. The attribute 42 | * must match the Angular pattern for event handlers (i.e. either (event)='blah()' or 43 | * on-event='blah' 44 | */ 45 | export function getEventHandler(preboot: PrebootRef, strategy: ListenStrategy, node: Element, eventName: string): Function { 46 | return function (event: DomEvent) { 47 | 48 | // if we aren't listening anymore (i.e. bootstrap complete) then don't capture any more events 49 | if (!state.listening) { return; } 50 | 51 | // we want to wait until client bootstraps so don't allow default action 52 | if (strategy.preventDefault) { 53 | event.preventDefault(); 54 | } 55 | 56 | // if we want to raise an event that others can listen for 57 | if (strategy.dispatchEvent) { 58 | preboot.dom.dispatchGlobalEvent(strategy.dispatchEvent); 59 | } 60 | 61 | // if callback provided for a custom action when an event occurs 62 | if (strategy.action) { 63 | strategy.action(preboot, node, event); 64 | } 65 | 66 | // when tracking focus keep a ref to the last active node 67 | if (strategy.trackFocus) { 68 | preboot.activeNode = caretPositionEvents.indexOf(eventName) >= 0 ? event.target : null; 69 | } 70 | 71 | // if event occurred that affects caret position in a node that we care about, record it 72 | if (caretPositionEvents.indexOf(eventName) >= 0 && 73 | caretPositionNodes.indexOf(node.tagName) >= 0) { 74 | 75 | preboot.selection = preboot.dom.getSelection(node); 76 | } 77 | 78 | // todo: need another solution for this hack 79 | if (eventName === 'keyup' && event.which === 13 && node.attributes['(keyup.enter)']) { 80 | preboot.dom.dispatchGlobalEvent('PrebootFreeze'); 81 | } 82 | 83 | // we will record events for later replay unless explicitly marked as doNotReplay 84 | if (!strategy.doNotReplay) { 85 | state.events.push({ 86 | node: node, 87 | event: event, 88 | name: eventName, 89 | time: preboot.time || (new Date()).getTime() 90 | }); 91 | } 92 | }; 93 | } 94 | 95 | /** 96 | * Loop through node events and add listeners 97 | */ 98 | export function addEventListeners(preboot: PrebootRef, nodeEvents: NodeEvent[], strategy: ListenStrategy) { 99 | for (let nodeEvent of nodeEvents) { 100 | let node = nodeEvent.node; 101 | let eventName = nodeEvent.eventName; 102 | let handler = getEventHandler(preboot, strategy, node, eventName); 103 | 104 | // add the actual event listener and keep a ref so we can remove the listener during cleanup 105 | node.addEventListener(eventName, handler); 106 | state.eventListeners.push({ 107 | node: node, 108 | name: eventName, 109 | handler: handler 110 | }); 111 | } 112 | } 113 | 114 | /** 115 | * Add event listeners based on node events found by the listen strategies. 116 | * Note that the getNodeEvents fn is gathered here without many safety 117 | * checks because we are doing all of those in src/server/normalize.ts. 118 | */ 119 | export function startListening(preboot: PrebootRef, opts: PrebootOptions) { 120 | state.listening = true; 121 | 122 | for (let strategy of opts.listen) { 123 | let getNodeEvents = strategy.getNodeEvents || strategies.listen[strategy.name].getNodeEvents; 124 | let nodeEvents = getNodeEvents(preboot, strategy); 125 | addEventListeners(preboot, nodeEvents, strategy); 126 | } 127 | } 128 | 129 | /** 130 | * Loop through replay strategies and call replayEvents functions. In most cases 131 | * there will be only one replay strategy, but users may want to add multiple in 132 | * some cases with the remaining events from one feeding into the next. 133 | * Note that as with startListening() above, there are very little safety checks 134 | * here in getting the replayEvents fn because those checks are in normalize.ts. 135 | */ 136 | export function replayEvents(preboot: PrebootRef, opts: PrebootOptions) { 137 | state.listening = false; 138 | 139 | for (let strategy of opts.replay) { 140 | let replayEvts = strategy.replayEvents || strategies.replay[strategy.name].replayEvents; 141 | state.events = replayEvts(preboot, strategy, state.events); 142 | } 143 | 144 | // it is probably an error if there are remaining events, but just log for now 145 | preboot.log(5, state.events); 146 | } 147 | 148 | /** 149 | * Go through all the event listeners and clean them up 150 | * by removing them from the given node (i.e. element) 151 | */ 152 | export function cleanup(preboot: PrebootRef, opts: PrebootOptions) { 153 | let activeNode = preboot.activeNode; 154 | 155 | // if there is an active node set, it means focus was tracked in one or more of the listen strategies 156 | if (activeNode) { 157 | let activeClientNode = preboot.dom.findClientNode(activeNode); 158 | if (activeClientNode) { 159 | preboot.dom.setSelection(activeClientNode, preboot.selection); 160 | } else { 161 | preboot.log(6, activeNode); 162 | } 163 | } 164 | 165 | // cleanup the event listeners 166 | for (let listener of state.eventListeners) { 167 | listener.node.removeEventListener(listener.name, listener.handler); 168 | } 169 | 170 | // finally clear out the events 171 | state.events = []; 172 | } 173 | -------------------------------------------------------------------------------- /modules/preboot/src/server/normalize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * this module is used to take input from the user on the server side 3 | * for the preboot options they want and to standarize those options 4 | * into a specific format that is known by the client code. 5 | */ 6 | import * as _ from 'lodash'; 7 | import presetFns from './presets'; 8 | import {PrebootOptions} from '../interfaces/preboot_options'; 9 | 10 | // these are the current pre-built strategies that are available 11 | export const listenStrategies = { attributes: true, event_bindings: true, selectors: true }; 12 | export const replayStrategies = { hydrate: true, rerender: true }; 13 | export const freezeStrategies = { spinner: true }; 14 | 15 | // this is just exposed for testing purposes 16 | export let defaultFreezeStyles = { 17 | overlay: { 18 | className: 'preboot-overlay', 19 | style: { 20 | position: 'absolute', 21 | display: 'none', 22 | zIndex: '9999999', 23 | top: '0', 24 | left: '0', 25 | width: '100%', 26 | height: '100%' 27 | } 28 | }, 29 | spinner: { 30 | className: 'preboot-spinner', 31 | style: { 32 | position: 'absolute', 33 | display: 'none', 34 | zIndex: '99999999' 35 | } 36 | } 37 | }; 38 | 39 | // this object contains functions for each PrebootOptions value to validate it 40 | // and prep it for call to generate client code 41 | export let normalizers = { 42 | 43 | /** 44 | * Just set default pauseEvent if doesn't exist 45 | */ 46 | pauseEvent: (opts: PrebootOptions) => { 47 | opts.pauseEvent = opts.pauseEvent || 'PrebootPause'; 48 | }, 49 | 50 | /** 51 | * Set default resumeEvent if doesn't exist 52 | */ 53 | resumeEvent: (opts: PrebootOptions) => { 54 | opts.resumeEvent = opts.resumeEvent || 'PrebootResume'; 55 | }, 56 | 57 | completeEvent: (opts: PrebootOptions) => { 58 | opts.completeEvent = opts.completeEvent || 'BootstrapComplete'; 59 | }, 60 | 61 | /** 62 | * Make sure that the listen option is an array of ListenStrategy 63 | * objects so client side doesn't need to worry about conversions 64 | */ 65 | listen: (opts: PrebootOptions) => { 66 | opts.listen = opts.listen || []; 67 | 68 | // if listen strategies are strings turn them into arrays 69 | if (typeof opts.listen === 'string') { 70 | if (!listenStrategies[opts.listen]) { 71 | throw new Error('Invalid listen strategy: ' + opts.listen); 72 | } else { 73 | opts.listen = [{ name: opts.listen }]; 74 | } 75 | } else if (!Array.isArray(opts.listen)) { 76 | opts.listen = [opts.listen]; 77 | } 78 | 79 | // loop through strategies and convert strings to objects 80 | opts.listen = opts.listen.map(function (val) { 81 | let strategy = (typeof val === 'string') ? { name: val } : val; 82 | 83 | if (strategy.name && !listenStrategies[strategy.name]) { 84 | throw new Error('Invalid listen strategy: ' + strategy.name); 85 | } else if (!strategy.name && !strategy.getNodeEvents) { 86 | throw new Error('Every listen strategy must either have a valid name or implement getNodeEvents()'); 87 | } 88 | 89 | return strategy; 90 | }); 91 | }, 92 | 93 | /** 94 | * Make sure replay options are array of ReplayStrategy objects. 95 | * So, callers can just pass in simple string, but converted to 96 | * an array before passed into client side preboot. 97 | */ 98 | replay: function (opts: PrebootOptions) { 99 | opts.replay = opts.replay || []; 100 | 101 | // if replay strategies are strings turn them into arrays 102 | if (typeof opts.replay === 'string') { 103 | if (!replayStrategies[opts.replay]) { 104 | throw new Error('Invalid replay strategy: ' + opts.replay); 105 | } else { 106 | opts.replay = [{ name: opts.replay }]; 107 | } 108 | } else if (!Array.isArray(opts.replay)) { 109 | opts.replay = [opts.replay]; 110 | } 111 | 112 | // loop through array and convert strings to objects 113 | opts.replay = opts.replay.map(function (val) { 114 | let strategy = (typeof val === 'string') ? { name: val } : val; 115 | 116 | if (strategy.name && !replayStrategies[strategy.name]) { 117 | throw new Error('Invalid replay strategy: ' + strategy.name); 118 | } else if (!strategy.name && !strategy.replayEvents) { 119 | throw new Error('Every replay strategy must either have a valid name or implement replayEvents()'); 120 | } 121 | 122 | return strategy; 123 | }); 124 | }, 125 | 126 | /** 127 | * Make sure freeze options are array of FreezeStrategy objects. 128 | * We have a set of base styles that are used for freeze (i.e. for 129 | * overaly and spinner), but these can be overriden 130 | */ 131 | freeze: function (opts: PrebootOptions) { 132 | 133 | // if no freeze option, don't do anything 134 | if (!opts.freeze) { return ; } 135 | 136 | let freezeName = opts.freeze.name || opts.freeze; 137 | let isFreezeNameString = (typeof freezeName === 'string'); 138 | 139 | // if freeze strategy doesn't exist, throw error 140 | if (isFreezeNameString && !freezeStrategies[freezeName]) { 141 | throw new Error('Invalid freeze option: ' + freezeName); 142 | } else if (!isFreezeNameString && (!opts.freeze.prep || !opts.freeze.cleanup)) { 143 | throw new Error('Freeze must have name or prep and cleanup functions'); 144 | } 145 | 146 | // if string convert to object 147 | if (typeof opts.freeze === 'string') { 148 | opts.freeze = { name: opts.freeze }; 149 | } 150 | 151 | // set default freeze values 152 | opts.freeze.styles = _.merge(defaultFreezeStyles, opts.freeze.styles); 153 | opts.freeze.eventName = opts.freeze.eventName || 'PrebootFreeze'; 154 | opts.freeze.timeout = opts.freeze.timeout || 5000; 155 | opts.freeze.doBlur = opts.freeze.doBlur === undefined ? true : opts.freeze.doBlur; 156 | }, 157 | 158 | /** 159 | * Presets are modifications to options. In the future, 160 | * we may be simple presets like 'angular' which add 161 | * all the listeners and replay. 162 | */ 163 | presets: function (opts: PrebootOptions) { 164 | let presetOptions = opts.presets; 165 | let presetName; 166 | 167 | // don't do anything if no presets 168 | if (!opts.presets) { return; } 169 | 170 | if (!Array.isArray(opts.presets)) { 171 | throw new Error('presets must be an array of strings'); 172 | } 173 | 174 | for (var i = 0; i < presetOptions.length; i++) { 175 | presetName = presetOptions[i]; 176 | 177 | if (!(typeof presetName === 'string')) { 178 | throw new Error('presets must be an array of strings'); 179 | } 180 | 181 | if (presetFns[presetName]) { 182 | presetFns[presetName](opts); 183 | } else { 184 | throw new Error('Invalid preset: ' + presetName); 185 | } 186 | } 187 | } 188 | }; 189 | 190 | /** 191 | * Normalize options so user can enter shorthand and it is 192 | * expanded as appropriate for the client code 193 | */ 194 | export function normalize(opts: PrebootOptions): PrebootOptions { 195 | opts = opts || {}; 196 | 197 | for (let key in normalizers) { 198 | if (normalizers.hasOwnProperty(key)) { 199 | normalizers[key](opts); 200 | } 201 | } 202 | 203 | // if no listen strategies, there is an issue because nothing will happen 204 | if (!opts.listen || !opts.listen.length) { 205 | throw new Error('Not listening for any events. Preboot not going to do anything.'); 206 | } 207 | 208 | return opts; 209 | } 210 | -------------------------------------------------------------------------------- /modules/preboot/src/client/dom.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a wrapper for the DOM that is used by preboot. We do this 3 | * for a few reasons. It makes the other preboot code more simple, 4 | * makes things easier to test (i.e. just mock out the DOM) and it 5 | * centralizes our DOM related interactions so we can more easily 6 | * add fixes for different browser quirks 7 | */ 8 | import {Element} from '../interfaces/element'; 9 | import {CursorSelection} from '../interfaces/preboot_ref'; 10 | 11 | export let nodeCache = {}; 12 | export let state = { 13 | window: null, 14 | document: null, 15 | body: null, 16 | appRoot: null, 17 | serverRoot: null, 18 | clientRoot: null 19 | }; 20 | 21 | /** 22 | * Initialize the DOM state based on input 23 | */ 24 | export function init(opts: any) { 25 | state.window = opts.window || state.window || {}; 26 | state.document = opts.document || (state.window && state.window.document) || {}; 27 | state.body = opts.body || (state.document && state.document.body); 28 | state.appRoot = opts.appRoot || state.body; 29 | state.serverRoot = state.clientRoot = state.appRoot; 30 | } 31 | 32 | /** 33 | * Setter for app root 34 | */ 35 | export function updateRoots(appRoot: Element, serverRoot?: Element, clientRoot?: Element) { 36 | state.appRoot = appRoot; 37 | state.serverRoot = serverRoot; 38 | state.clientRoot = clientRoot; 39 | } 40 | 41 | /** 42 | * Get a node in the document 43 | */ 44 | export function getDocumentNode(selector: string): Element { 45 | return state.document.querySelector(selector); 46 | } 47 | 48 | /** 49 | * Get one app node 50 | */ 51 | export function getAppNode(selector: string): Element { 52 | return state.appRoot.querySelector(selector); 53 | } 54 | 55 | /** 56 | * Get all app nodes for a given selector 57 | */ 58 | export function getAllAppNodes(selector: string): Element[] { 59 | return state.appRoot.querySelectorAll(selector); 60 | } 61 | 62 | /** 63 | * Get all nodes under the client root 64 | */ 65 | export function getClientNodes(selector: string): Element[] { 66 | return state.clientRoot.querySelectorAll(selector); 67 | } 68 | 69 | /** 70 | * Add event listener at window level 71 | */ 72 | export function onLoad(handler: Function) { 73 | state.window.addEventListener('load', handler); 74 | } 75 | 76 | /** 77 | * These are global events that get passed around. Currently 78 | * we use the document to do this. 79 | */ 80 | export function on(eventName: string, handler: Function) { 81 | state.document.addEventListener(eventName, handler); 82 | } 83 | 84 | /** 85 | * Dispatch an event on the document 86 | */ 87 | export function dispatchGlobalEvent(eventName: string) { 88 | state.document.dispatchEvent(new state.window.Event(eventName)); 89 | } 90 | 91 | /** 92 | * Dispatch an event on a specific node 93 | */ 94 | export function dispatchNodeEvent(node: Element, eventName: string) { 95 | node.dispatchEvent(new state.window.Event(eventName)); 96 | } 97 | 98 | /** 99 | * Check to see if the app contains a particular node 100 | */ 101 | export function appContains(node: Element) { 102 | return state.appRoot.contains(node); 103 | } 104 | 105 | /** 106 | * Create a new element 107 | */ 108 | export function addNodeToBody(type: string, className: string, styles: Object): Element { 109 | let elem = state.document.createElement(type); 110 | elem.className = className; 111 | 112 | if (styles) { 113 | for (var key in styles) { 114 | if (styles.hasOwnProperty(key)) { 115 | elem.style[key] = styles[key]; 116 | } 117 | } 118 | } 119 | 120 | return state.body.appendChild(elem); 121 | } 122 | 123 | /** 124 | * Remove a node since we are done with it 125 | */ 126 | export function removeNode(node: Element) { 127 | if (!node) { return; } 128 | 129 | node.remove ? 130 | node.remove() : 131 | node.style.display = 'none'; 132 | } 133 | 134 | /** 135 | * Get the caret position within a given node. Some hackery in 136 | * here to make sure this works in all browsers 137 | */ 138 | export function getSelection(node: Element): CursorSelection { 139 | let selection = { 140 | start: 0, 141 | end: 0, 142 | direction: 'forward' 143 | }; 144 | 145 | // if browser support selectionStart on node (Chrome, FireFox, IE9+) 146 | if (node && (node.selectionStart || node.selectionStart === 0)) { 147 | selection.start = node.selectionStart; 148 | selection.end = node.selectionEnd; 149 | selection.direction = node.selectionDirection; 150 | 151 | // else if nothing else for older unsupported browsers, just put caret at the end of the text 152 | } else if (node && node.value) { 153 | selection.start = selection.end = node.value.length; 154 | } 155 | 156 | return selection; 157 | } 158 | 159 | /** 160 | * Set caret position in a given node 161 | */ 162 | export function setSelection(node: Element, selection: CursorSelection) { 163 | 164 | // as long as node exists, set focus 165 | if (node) { 166 | node.focus(); 167 | } 168 | 169 | // set selection if a modern browser (i.e. IE9+, etc.) 170 | if (node && node.setSelectionRange && selection) { 171 | node.setSelectionRange(selection.start, selection.end, selection.direction); 172 | } 173 | } 174 | 175 | /** 176 | * Get a unique key for a node in the DOM 177 | */ 178 | export function getNodeKey(node: Element, rootNode: Element): string { 179 | let ancestors = []; 180 | let temp = node; 181 | while (temp && temp !== rootNode) { 182 | ancestors.push(temp); 183 | temp = temp.parentNode; 184 | } 185 | 186 | // push the rootNode on the ancestors 187 | if (temp) { 188 | ancestors.push(temp); 189 | } 190 | 191 | // now go backwards starting from the root 192 | let key = node.nodeName; 193 | let len = ancestors.length; 194 | 195 | for (let i = (len - 1); i >= 0; i--) { 196 | temp = ancestors[i]; 197 | 198 | if (temp.childNodes && i > 0) { 199 | for (let j = 0; j < temp.childNodes.length; j++) { 200 | if (temp.childNodes[j] === ancestors[i - 1]) { 201 | key += '_s' + (j + 1); 202 | break; 203 | } 204 | } 205 | } 206 | } 207 | 208 | return key; 209 | } 210 | 211 | /** 212 | * Given a node from the server rendered view, find the equivalent 213 | * node in the client rendered view. 214 | */ 215 | export function findClientNode(serverNode: Element): Element { 216 | 217 | // if nothing passed in, then no client node 218 | if (!serverNode) { return null; } 219 | 220 | // we use the string of the node to compare to the client node & as key in cache 221 | let serverNodeKey = getNodeKey(serverNode, state.serverRoot); 222 | 223 | // first check to see if we already mapped this node 224 | let nodes = nodeCache[serverNodeKey] || []; 225 | 226 | for (let nodeMap of nodes) { 227 | if (nodeMap.serverNode === serverNode) { 228 | return nodeMap.clientNode; 229 | } 230 | } 231 | 232 | // todo: improve this algorithm in the future so uses fuzzy logic (i.e. not necessarily perfect match) 233 | let selector = serverNode.tagName; 234 | let className = (serverNode.className || '').replace('ng-binding', '').trim(); 235 | 236 | if (serverNode.id) { 237 | selector += '#' + serverNode.id; 238 | } else if (className) { 239 | selector += '.' + className.replace(/ /g, '.'); 240 | } 241 | 242 | let clientNodes = getClientNodes(selector); 243 | for (let clientNode of clientNodes) { 244 | 245 | // todo: this assumes a perfect match which isn't necessarily true 246 | if (getNodeKey(clientNode, state.clientRoot) === serverNodeKey) { 247 | 248 | // add the client/server node pair to the cache 249 | nodeCache[serverNodeKey] = nodeCache[serverNodeKey] || []; 250 | nodeCache[serverNodeKey].push({ 251 | clientNode: clientNode, 252 | serverNode: serverNode 253 | }); 254 | 255 | return clientNode; 256 | } 257 | } 258 | 259 | // if we get here it means we couldn't find the client node 260 | return null; 261 | } 262 | -------------------------------------------------------------------------------- /modules/preboot/test/server/normalize_spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import {normalize, normalizers, defaultFreezeStyles} from '../../src/server/normalize'; 4 | 5 | describe('normalize', function () { 6 | 7 | describe('pauseEvent()', function () { 8 | it('should verify default', function () { 9 | let opts = { pauseEvent: '' }; 10 | normalizers.pauseEvent(opts); 11 | expect(opts.pauseEvent).toBe('PrebootPause'); 12 | }); 13 | 14 | it('should set value', function () { 15 | let opts = { pauseEvent: 'BlahEvt' }; 16 | normalizers.pauseEvent(opts); 17 | expect(opts.pauseEvent).toBe('BlahEvt'); 18 | }); 19 | }); 20 | 21 | describe('resumeEvent()', function () { 22 | it('should verify default', function () { 23 | let opts = { resumeEvent: '' }; 24 | normalizers.resumeEvent(opts); 25 | expect(opts.resumeEvent).toBe('PrebootResume'); 26 | }); 27 | 28 | it('should set value', function () { 29 | let opts = { resumeEvent: 'foo' }; 30 | normalizers.resumeEvent(opts); 31 | expect(opts.resumeEvent).toBe('foo'); 32 | }); 33 | }); 34 | 35 | describe('listen()', function () { 36 | it('should verify default', function () { 37 | let opts = { listen: null }; 38 | normalizers.listen(opts); 39 | expect(opts.listen).toEqual([]); 40 | }); 41 | 42 | it('should throw an error if string not valid listen strategy', function () { 43 | let opts = { listen: 'blah' }; 44 | let fn = () => normalizers.listen(opts); 45 | expect(fn).toThrowError('Invalid listen strategy: blah'); 46 | }); 47 | 48 | it('should convert string to array', function () { 49 | let opts = { listen: 'event_bindings' }; 50 | normalizers.listen(opts); 51 | expect(opts.listen).toEqual([{ name: 'event_bindings' }]); 52 | }); 53 | 54 | it('should throw error if no name or getNodeEvents', function () { 55 | let listen = { foo: 'zoo' }; 56 | let opts = { listen: listen }; 57 | let fn = () => normalizers.listen(opts); 58 | expect(fn).toThrowError('Every listen strategy must either have a valid name or implement getNodeEvents()'); 59 | }); 60 | 61 | /* tslint:disable:no-empty */ 62 | it('should convert object to array with getNodeEvents impl', function () { 63 | let listen = { foo: 'blue', getNodeEvents: function () {} }; 64 | let opts = { listen: listen }; 65 | normalizers.listen(opts); 66 | expect(opts.listen).toEqual([listen]); 67 | }); 68 | 69 | it('should throw error if invalid name', function () { 70 | let listen = [{ name: 'asdfsd', foo: 'shoo' }]; 71 | let opts = { listen: listen }; 72 | let fn = () => normalizers.listen(opts); 73 | expect(fn).toThrowError('Invalid listen strategy: ' + 'asdfsd'); 74 | }); 75 | 76 | it('should use array if valid', function () { 77 | let listen = [ 78 | { name: 'event_bindings', foo: 'shoo' }, 79 | { getNodeEvents: function () {}, foo: 'sdfsd' } 80 | ]; 81 | let opts = { listen: listen }; 82 | normalizers.listen(opts); 83 | expect(opts.listen).toEqual(listen); 84 | }); 85 | }); 86 | 87 | describe('replay()', function () { 88 | it('should verify default', function () { 89 | let opts = { replay: null }; 90 | normalizers.replay(opts); 91 | expect(opts.replay).toEqual([]); 92 | }); 93 | 94 | it('should throw an error if string not valid replay strategy', function () { 95 | let opts = { replay: 'blah' }; 96 | let fn = () => normalizers.replay(opts); 97 | expect(fn).toThrowError('Invalid replay strategy: blah'); 98 | }); 99 | 100 | it('should convert string to array', function () { 101 | let opts = { replay: 'rerender' }; 102 | normalizers.replay(opts); 103 | expect(opts.replay).toEqual([{ name: 'rerender' }]); 104 | }); 105 | 106 | it('should throw error if no name or replayEvents', function () { 107 | let replay = { foo: 'zoo' }; 108 | let opts = { replay: replay }; 109 | let fn = () => normalizers.replay(opts); 110 | expect(fn).toThrowError('Every replay strategy must either have a valid name or implement replayEvents()'); 111 | }); 112 | 113 | it('should convert object to array with replayEvents impl', function () { 114 | let replay = { foo: 'blue', replayEvents: function () {} }; 115 | let opts = { replay: replay }; 116 | normalizers.replay(opts); 117 | expect(opts.replay).toEqual([replay]); 118 | }); 119 | 120 | it('should throw error if invalid name', function () { 121 | let replay = [{ name: 'asdfsd', foo: 'shoo' }]; 122 | let opts = { replay: replay }; 123 | let fn = () => normalizers.replay(opts); 124 | expect(fn).toThrowError('Invalid replay strategy: ' + 'asdfsd'); 125 | }); 126 | 127 | it('should use array if valid', function () { 128 | let replay = [ 129 | { name: 'hydrate', foo: 'shoo' }, 130 | { replayEvents: function () {}, foo: 'sdfsd' } 131 | ]; 132 | let opts = { replay: replay }; 133 | normalizers.replay(opts); 134 | expect(opts.replay).toEqual(replay); 135 | }); 136 | }); 137 | 138 | describe('freeze()', function () { 139 | it('should do nothing if no freeze option', function () { 140 | let opts = {}; 141 | normalizers.freeze(opts); 142 | expect(opts).toEqual({}); 143 | }); 144 | 145 | it('should throw error if invalid freeze strategy', function () { 146 | let opts = { freeze: 'asdf' }; 147 | let fn = () => normalizers.freeze(opts); 148 | expect(fn).toThrowError('Invalid freeze option: asdf'); 149 | }); 150 | 151 | it('should throw error if no string and no prep and cleanup', function () { 152 | let opts = { freeze: {} }; 153 | let fn = () => normalizers.freeze(opts); 154 | expect(fn).toThrowError('Freeze must have name or prep and cleanup functions'); 155 | }); 156 | 157 | it('should have default styles if valid freeze', function () { 158 | let opts = { freeze: { name: 'spinner', styles: {} } }; 159 | normalizers.freeze(opts); 160 | expect(opts.freeze.styles).toEqual(defaultFreezeStyles); 161 | }); 162 | 163 | it('should override default styles', function () { 164 | let freezeStyleOverrides = { 165 | overlay: { className: 'foo' }, 166 | spinner: { className: 'zoo' } 167 | }; 168 | let opts = { freeze: { name: 'spinner', styles: freezeStyleOverrides } }; 169 | normalizers.freeze(opts); 170 | expect(opts.freeze.styles.overlay.className).toEqual(freezeStyleOverrides.overlay.className); 171 | expect(opts.freeze.styles.spinner.className).toEqual(defaultFreezeStyles.spinner.className); 172 | }); 173 | }); 174 | 175 | describe('presets()', function () { 176 | it('should do nothing if no presets option', function () { 177 | let opts = {}; 178 | normalizers.presets(opts); 179 | expect(opts).toEqual({}); 180 | }); 181 | 182 | it('should throw error if presets not an array', function () { 183 | let opts = { presets: 'asdf' }; 184 | let fn = () => normalizers.presets(opts); 185 | expect(fn).toThrowError('presets must be an array of strings'); 186 | }); 187 | 188 | it('should throw error if presets not an array', function () { 189 | let opts = { presets: [{}] }; 190 | let fn = () => normalizers.presets(opts); 191 | expect(fn).toThrowError('presets must be an array of strings'); 192 | }); 193 | 194 | it('should throw error if invalid preset value', function () { 195 | let opts = { presets: ['asdfsd'] }; 196 | let fn = () => normalizers.presets(opts); 197 | expect(fn).toThrowError('Invalid preset: asdfsd'); 198 | }); 199 | }); 200 | 201 | describe('normalize()', function () { 202 | it('should throw error if not listening for events', function () { 203 | let opts = {}; 204 | let fn = () => normalize(opts); 205 | expect(fn).toThrowError('Not listening for any events. Preboot not going to do anything.'); 206 | }); 207 | }); 208 | }); 209 | -------------------------------------------------------------------------------- /modules/universal/server/src/http/server_http.ts: -------------------------------------------------------------------------------- 1 | import '../server_patch'; 2 | 3 | import { 4 | provide, 5 | OpaqueToken, 6 | Injectable, 7 | Optional, 8 | Inject, 9 | EventEmitter, 10 | NgZone 11 | } from 'angular2/core'; 12 | 13 | import { 14 | Observable 15 | } from 'rxjs'; 16 | 17 | import { 18 | Http, 19 | Connection, 20 | ConnectionBackend, 21 | // XHRConnection, 22 | XHRBackend, 23 | RequestOptions, 24 | ResponseType, 25 | ResponseOptions, 26 | ResponseOptionsArgs, 27 | RequestOptionsArgs, 28 | BaseResponseOptions, 29 | BaseRequestOptions, 30 | Request, 31 | Response, 32 | ReadyState, 33 | BrowserXhr, 34 | RequestMethod 35 | } from 'angular2/http'; 36 | import { 37 | MockBackend 38 | } from 'angular2/src/http/backends/mock_backend'; 39 | 40 | import {ObservableWrapper} from 'angular2/src/facade/async'; 41 | 42 | import { 43 | isPresent, 44 | isBlank, 45 | CONST_EXPR 46 | } from 'angular2/src/facade/lang'; 47 | 48 | var Rx = require('rxjs'); 49 | 50 | // CJS 51 | import XMLHttpRequest = require('xhr2'); 52 | 53 | 54 | export const BASE_URL: OpaqueToken = CONST_EXPR(new OpaqueToken('baseUrl')); 55 | 56 | export const PRIME_CACHE: OpaqueToken = CONST_EXPR(new OpaqueToken('primeCache')); 57 | 58 | 59 | class NodeConnection implements Connection { 60 | request: Request; 61 | /** 62 | * Response {@link EventEmitter} which emits a single {@link Response} value on load event of 63 | * `XMLHttpRequest`. 64 | */ 65 | response: any; // TODO: Make generic of ; 66 | readyState: ReadyState; 67 | constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) { 68 | this.request = req; 69 | this.response = new Observable(responseObserver => { 70 | let _xhr: any = browserXHR.build(); 71 | _xhr.open(RequestMethod[req.method].toUpperCase(), req.url); 72 | // load event handler 73 | let onLoad = () => { 74 | // responseText is the old-school way of retrieving response (supported by IE8 & 9) 75 | // response/responseType properties were introduced in XHR Level2 spec (supported by 76 | // IE10) 77 | let response = ('response' in _xhr) ? _xhr.response : _xhr.responseText; 78 | 79 | // normalize IE9 bug (http://bugs.jquery.com/ticket/1450) 80 | let status = _xhr.status === 1223 ? 204 : _xhr.status; 81 | 82 | // fix status code when it is 0 (0 status is undocumented). 83 | // Occurs when accessing file resources or on Android 4.1 stock browser 84 | // while retrieving files from application cache. 85 | if (status === 0) { 86 | status = response ? 200 : 0; 87 | } 88 | var responseOptions = new ResponseOptions({body: response, status: status}); 89 | if (isPresent(baseResponseOptions)) { 90 | responseOptions = baseResponseOptions.merge(responseOptions); 91 | } 92 | responseObserver.next(new Response(responseOptions)); 93 | // TODO(gdi2290): defer complete if array buffer until done 94 | responseObserver.complete(); 95 | }; 96 | // error event handler 97 | let onError = (err) => { 98 | var responseOptions = new ResponseOptions({body: err, type: ResponseType.Error}); 99 | if (isPresent(baseResponseOptions)) { 100 | responseOptions = baseResponseOptions.merge(responseOptions); 101 | } 102 | responseObserver.error(new Response(responseOptions)); 103 | }; 104 | 105 | if (isPresent(req.headers)) { 106 | req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(','))); 107 | } 108 | 109 | _xhr.addEventListener('load', onLoad); 110 | _xhr.addEventListener('error', onError); 111 | 112 | _xhr.send(this.request.text()); 113 | 114 | return () => { 115 | _xhr.removeEventListener('load', onLoad); 116 | _xhr.removeEventListener('error', onError); 117 | _xhr.abort(); 118 | }; 119 | }); 120 | } 121 | } 122 | 123 | 124 | @Injectable() 125 | export class NodeXhr { 126 | _baseUrl: string; 127 | constructor(@Optional() @Inject(BASE_URL) baseUrl?: string) { 128 | 129 | if (isBlank(baseUrl)) { 130 | throw new Error('No base url set. Please provide a BASE_URL bindings.'); 131 | } 132 | 133 | this._baseUrl = baseUrl; 134 | 135 | } 136 | build() { 137 | let xhr = new XMLHttpRequest(); 138 | xhr.nodejsSet({ baseUrl: this._baseUrl }); 139 | return xhr; 140 | } 141 | } 142 | 143 | @Injectable() 144 | export class NodeBackend { 145 | constructor(private _browserXHR: BrowserXhr, private _baseResponseOptions: ResponseOptions) { 146 | } 147 | createConnection(request: any): Connection { 148 | return new NodeConnection(request, this._browserXHR, this._baseResponseOptions); 149 | } 150 | } 151 | 152 | 153 | @Injectable() 154 | export class NgPreloadCacheHttp extends Http { 155 | _async: number = 0; 156 | _callId: number = 0; 157 | _rootNode; 158 | _activeNode; 159 | constructor( 160 | protected _backend: ConnectionBackend, 161 | protected _defaultOptions: RequestOptions, 162 | @Inject(NgZone) protected _ngZone: NgZone, 163 | @Optional() @Inject(PRIME_CACHE) protected prime?: boolean 164 | ) { 165 | super(_backend, _defaultOptions); 166 | 167 | var _rootNode = { children: [], res: null }; 168 | this._rootNode = _rootNode; 169 | this._activeNode = _rootNode; 170 | 171 | 172 | } 173 | 174 | preload(factory) { 175 | 176 | // TODO: fix this after the next release with RxNext 177 | var obs = new EventEmitter(); 178 | 179 | var currentNode = null; 180 | if (isPresent(this._activeNode)) { 181 | currentNode = { children: [], res: null }; 182 | this._activeNode.children.push(currentNode); 183 | } 184 | 185 | // We need this to ensure all ajax calls are done before rendering the app 186 | this._async += 1; 187 | var request = factory(); 188 | 189 | request.subscribe({ 190 | next: () => { 191 | 192 | }, 193 | error: () => { 194 | this._ngZone.run(() => { 195 | setTimeout(() => { this._async -= 1; }); 196 | }); 197 | }, 198 | complete: () => { 199 | this._ngZone.run(() => { 200 | setTimeout(() => { this._async -= 1; }); 201 | }); 202 | } 203 | }); 204 | 205 | return request; 206 | } 207 | 208 | request(url: string | Request, options?: RequestOptionsArgs): Observable { 209 | return isBlank(this.prime) ? super.request(url, options) : this.preload(() => super.request(url, options)); 210 | } 211 | 212 | get(url: string, options?: RequestOptionsArgs): Observable { 213 | return isBlank(this.prime) ? super.get(url, options) : this.preload(() => super.get(url, options)); 214 | 215 | } 216 | 217 | post(url: string, body: string, options?: RequestOptionsArgs): Observable { 218 | return isBlank(this.prime) ? super.post(url, body, options) : this.preload(() => super.post(url, body, options)); 219 | } 220 | 221 | put(url: string, body: string, options?: RequestOptionsArgs): Observable { 222 | return isBlank(this.prime) ? super.put(url, body, options) : this.preload(() => super.put(url, body, options)); 223 | } 224 | 225 | delete(url: string, options?: RequestOptionsArgs): Observable { 226 | return isBlank(this.prime) ? super.delete(url, options) : this.preload(() => super.delete(url, options)); 227 | 228 | } 229 | 230 | patch(url: string, body: string, options?: RequestOptionsArgs): Observable { 231 | return isBlank(this.prime) ? super.patch(url, body, options) : this.preload(() => super.patch(url, body, options)); 232 | } 233 | 234 | head(url: string, options?: RequestOptionsArgs): Observable { 235 | return isBlank(this.prime) ? super.head(url, options) : this.preload(() => super.head(url, options)); 236 | } 237 | 238 | 239 | } 240 | 241 | 242 | export var HTTP_PROVIDERS: Array = [ 243 | provide(BASE_URL, {useValue: ''}), 244 | provide(PRIME_CACHE, {useValue: false}), 245 | provide(RequestOptions, {useClass: BaseRequestOptions}), 246 | provide(ResponseOptions, {useClass: BaseResponseOptions}), 247 | 248 | provide(BrowserXhr, {useClass: NodeXhr}), 249 | provide(ConnectionBackend, {useClass: NodeBackend}), 250 | 251 | provide(Http, {useClass: NgPreloadCacheHttp}) 252 | ]; 253 | --------------------------------------------------------------------------------