UNPKG

10.5 kBMarkdownView Raw
1# preboot
2
3The purpose of this library is to help manage the transition of state (i.e. events, focus, data) from a server-generated web view to a client-generated web view. This library works with any front end JavaScript framework (i.e. React, Vue, Ember, etc.), but does have a few extra convenience modules for Angular apps.
4
5The key features of preboot include:
6
71. Record and play back events
81. Respond immediately to certain events in the server view
91. Maintain focus even page is re-rendered
101. Buffer client-side re-rendering for smoother transition
111. Freeze page until bootstrap complete for certain events (ex. form submission)
12
13In essence, this library is all about managing the user experience from the time from when
14a server view is visible until the client view takes over control of the page.
15
16## Installation
17
18cd into your app root and run the following command:
19
20```
21npm i preboot --save
22```
23
24The following sections covers the three different configurations of preboot:
25
26- Angular Configuration
27- Non-Angular Server Configuration
28- Non-Angular Browser Configuration
29
30#### Angular Configuration
31
32```ts
33import { NgModule } from '@angular/core';
34import { BrowserModule } from '@angular/platform-browser';
35import { PrebootModule } from 'preboot';
36import { AppComponent } from './app.component';
37
38@NgModule({
39 declarations: [AppComponent],
40 imports: [
41 BrowserModule.withServerTransition({ appId: 'foo' }),
42 PrebootModule.withConfig({ appRoot: 'app-root' })
43 ],
44 bootstrap: [AppComponent]
45})
46export class AppModule { }
47```
48
49The key part here for preboot is to include `PrebootModule.withConfig({ appRoot: 'app-root' })` where the `appRoot` is the selector(s) to find the root of your application. The options you can pass into `withConfig()` are in the [PrebootOptions](#PrebootOptions) section below. In most cases, however, you will only need to specify the `appRoot`.
50
51#### Non-Angular Server Configuration
52
53```ts
54import { getInlineDefinition, getInlineInvocation } from 'preboot';
55
56const prebootOptions = {}; // see PrebootRecordOptions section below
57const inlineCodeDefinition = getInlineDefinition(prebootOptions);
58const inlineCodeInvocation = getInlineInvocation();
59
60// now insert `inlineCodeDefinition` into a `<script>` tag in `<head>` and
61// an `inlineCodeInvocation` copy just after the opening tag of each app root
62
63```
64
65```html
66<html>
67 <head>
68 <script><%= inlineCodeDefinition %></script>
69 </head>
70 <body>
71 <app1-root>
72 <script><%= inlineCodeInvocation %></script>
73 <h2>App1 header</h2>
74 <div>content</div>
75 </app1-root>
76 <app2-root>
77 <script><%= inlineCodeInvocation %></script>
78 <h2>App2 header</h2>
79 <span>content</span>
80 </app2-root>
81 </body>
82</html>
83```
84
85#### Non-Angular Browser Configuration
86
87```ts
88import { EventReplayer } from 'preboot';
89
90const replayer = new EventReplayer();
91
92// once you are ready to replay events (usually after client app fully loaded)
93replayer.replayAll();
94```
95
96#### PrebootOptions
97
98* `appRoot` (**required**) - One or more selectors for apps in the page (i.e. so one string or an array of strings).
99* `buffer` (default `true`) - If true, preboot will attempt to buffer client rendering to an extra hidden div. In most
100cases you will want to leave the default (i.e. true) but may turn off if you are debugging an issue.
101* `minify` (deprecated) - minification has been removed in v6. Minification should be handled by the end-user
102* `disableOverlay` (default `true`) - If true, freeze overlay would not get injected in the DOM.
103* `eventSelectors` (defaults below) - This is an array of objects which specify what events preboot should be listening for
104on the server view and how preboot should replay those events to the client view.
105See Event Selector section below for more details but note that in most cases, you can just rely on the defaults
106and you don't need to explicitly set anything here.
107* `replay` (default `true`) - The only reason why you would want to set this to `false` is if you want to
108manually trigger the replay yourself. This contrasts with the event selector `replay`, because this option is global
109
110This comes in handy for situations where you want to hold off
111on the replay and buffer switch until AFTER some async events occur (i.e. route loading, http calls, etc.). By
112default, replay occurs right after bootstrap is complete. In some apps, there are more events after bootstrap
113however where the page continues to change in significant ways. Basically if you are making major changes to
114the page after bootstrap then you will see some jank unless you set `replay` to `false` and then trigger replay
115yourself once you know that all async events are complete.
116
117To manually trigger replay, inject the EventReplayer like this:
118
119```ts
120import { Injectable } from '@angular/core';
121import { EventReplayer } from 'preboot';
122
123@Injectable()
124class Foo {
125 constructor(private replayer: EventReplayer) {}
126
127 // you decide when to call this based on what your app is doing
128 manualReplay() {
129 this.replayer.replayAll();
130 }
131}
132```
133
134**Event Selectors**
135
136This part of the options drives a lot of the core behavior of preboot.
137Each event selector has the following properties:
138
139* `selector` - The selector to find nodes under the server root (ex. `input, .blah, #foo`)
140* `events` - An array of event names to listen for (ex. `['focusin', 'keyup', 'click']`)
141* `keyCodes` - Only do something IF event includes a key pressed that matches the given key codes.
142Useful for doing something when user hits return in a input box or something similar.
143* `preventDefault` - If `true`, `event.preventDefault()` will be called to prevent any further event propagation.
144* `freeze` - If `true`, the UI will freeze which means displaying a translucent overlay which prevents
145any further user action until preboot is complete.
146* `action` - This is a function callback for any custom code you want to run when this event occurs
147in the server view.
148* `replay` - If `false`, the event won't be recorded or replayed. Useful when you utilize one of the other options above.
149
150Here are some examples of event selectors from the defaults:
151
152```es6
153var eventSelectors = [
154
155 // for recording changes in form elements
156 { selector: 'input,textarea', events: ['keypress', 'keyup', 'keydown', 'input', 'change'] },
157 { selector: 'select,option', events: ['change'] },
158
159 // when user hits return button in an input box
160 { selector: 'input', events: ['keyup'], preventDefault: true, keyCodes: [13], freeze: true },
161
162 // when user submit form (press enter, click on button/input[type="submit"])
163 { selector: 'form', events: ['submit'], preventDefault: true, freeze: true },
164
165 // for tracking focus (no need to replay)
166 { selector: 'input,textarea', events: ['focusin', 'focusout', 'mousedown', 'mouseup'], replay: false },
167
168 // user clicks on a button
169 { selector: 'button', events: ['click'], preventDefault: true, freeze: true }
170];
171```
172
173#### Using with manual bootstrap (e.g. with ngUpgrade)
174
175Preboot registers its reply code at the `APP_BOOTSTRAP_LISTENER` token which is called by Angular for every component that is bootstrapped. If you don't have the `bootstrap` property defined in your `AppModule`'s `NgModule` but you instead use the `ngDoBootrap` method (which is done e.g. when using ngUpgrade) this code will not run at all.
176
177To make Preboot work correctly in such a case you need to specify `replay: false` in the Preboot options and replay the events yourself. That is, import `PrebootModule` like this:
178
179```ts
180PrebootModule.withConfig({
181 appRoot: 'app-root',
182 replay: false,
183})
184```
185
186and replay events in `AppComponent` like this:
187
188```ts
189import { isPlatformBrowser } from '@angular/common';
190import { Component, OnInit, PLATFORM_ID, Inject, ApplicationRef } from '@angular/core';
191import { Router } from '@angular/router';
192import { filter, take } from 'rxjs/operators';
193import { EventReplayer } from 'preboot';
194
195@Component({
196 selector: 'app-root',
197 templateUrl: './app.component.html',
198 styleUrls: ['./app.component.scss'],
199})
200export class AppComponent implements OnInit {
201 constructor(
202 private router: Router,
203 @Inject(PLATFORM_ID) private platformId: string,
204 private appRef: ApplicationRef,
205 private replayer: EventReplayer,
206 ) {}
207
208 ngOnInit() {
209 this.router.initialNavigation();
210 if (isPlatformBrowser(this.platformId)) {
211 this.appRef.isStable
212 .pipe(
213 filter(stable => stable),
214 take(1),
215 ).subscribe(() => {
216 this.replayer.replayAll();
217 });
218 }
219 }
220}
221```
222
223#### PrebootComplete
224
225When you are manually replaying events, you often will want to know when Preboot
226is done replaying events and switching the buffers. To do this, use the following
227code in your app:
228
229```es6
230window.document.addEventListener('PrebootComplete', () => {
231 // put your code here that you want to run once preboot is complete
232});
233```
234
235#### Adding a nonce
236
237If you're using a CSP, you'll need to add a `nonce` property to preboot's inline script.
238Preboot allows you to configure this by exporting an optional `PREBOOT_NONCE` token.
239Example usage is as follows (for an Express server):
240
241```ts
242import {PREBOOT_NONCE} from 'preboot';
243import * as express from 'express';
244import {v4} from 'uuid';
245import * as csp from 'helmet-csp';
246
247const app = express();
248
249app.use((req, res, next) => {
250 res.locals.nonce = v4();
251 next();
252});
253
254app.use(csp({
255 directives: {
256 scriptSrc: [`'self'`, (req, res) => `'nonce-${ res.locals.nonce }'`],
257 ...
258 }
259});
260
261... express boilerplate ...
262
263/* when it comes time to render the request, we can inject our new token */
264
265app.get('*', (req, res) => {
266 res.render('index', {
267 req,
268 res,
269 providers: [
270 {
271 provide: PREBOOT_NONCE,
272 useValue: res.locals.nonce
273 }
274 ]
275 });
276});
277
278... other express route handling (see Universal guide for details) ...
279```
280
281Please note that only the nonce tag will appear on the script,
282**the value is not rendered in modern browsers**. If you want to make
283sure your nonce is generating correctly, you can add a callback
284onto your render method to examine the resultant HTML as follows:
285
286```ts
287res.render('index', (req, res) => {
288 ...
289}, function(error, html) {
290 console.log(html.substring(0, 50)); // we only care about the top part
291 res.send(html);
292});
293```
294
295#### Browser support
296
297If you wish to support Internet Explorer 9-11, you will need to include a [Polyfill](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill) for `CustomEvent`.