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 | 
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 | [](https://www.youtube.com/watch?v=MtoHFDfi8FM)
25 |
26 | Angular 2 Server Rendering - Angular U, July 2015
27 | [](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 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
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 | [](https://travis-ci.org/angular/universal)
2 | [](http://badge.fury.io/js/angular2-universal-preview)
3 | [](https://gitter.im/angular/universal?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](http://issuestats.com/github/angular/universal)
5 | [](http://issuestats.com/github/angular/universal)
6 |
7 |
8 |
9 |
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 | [](https://www.youtube.com/watch?v=MtoHFDfi8FM)
35 |
36 | Angular 2 Server Rendering - Angular U, July 2015
37 | [](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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------