-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrender-to-html.js
114 lines (97 loc) · 3.86 KB
/
render-to-html.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import {combineAsArray} from 'baconjs';
import uuid from 'uuid/v1';
import serialize from 'serialize-javascript';
import {PhaseContext, ServerContext, SERVER} from './context';
const defaultRender = ReactDOMServer.renderToString;
/**
* Asynchronously render a widget element to HTML, also rendering a script tag that stores <code>props</code> and
* <code>hydration</code> for hydration in the browser.
*
* If the specified React element isn't a widget, just render it as is.
*
* @param {Object} widgetElement - an instance of a widget
* @param {Object} [options] - (see below)
* @param {Function} [options.render] - an optional alternative server renderer
* @param {string} [options.className] - an optional class name for the mount point
* @returns {Promise.<string>} a promise that will resolve to the element's HTML
*/
export default async function renderToHtml(
widgetElement,
{
render = defaultRender,
className,
onData,
} = {}
) {
const {__widget_name__: name} = widgetElement.type;
// If this isn't a widget, rather than crapping out, just render it as is.
if (!name) {
return `<div${className ? ` class="${className}"` : ''}>${render(widgetElement)}</div>`;
}
const registeredStreams = {};
const pendingKeys = new Set();
const hydration = {};
const getStream = (key) => registeredStreams[key];
// Register stream. In the background, this stores the initial event in hydration, then deregisters the stream.
const registerStream = (key, stream$) => {
registeredStreams[key] = stream$;
pendingKeys.add(key);
};
let error;
const onError = (e) => {
error = e;
};
// Start walking the element tree.
ReactDOMServer.renderToStaticMarkup((
<PhaseContext.Provider value={() => SERVER}>
<ServerContext.Provider value={{getStream, registerStream, onError}}>
{widgetElement}
</ServerContext.Provider>
</PhaseContext.Provider>
));
// Rethrow immediately produced error
if (error) {
throw error;
}
// Keep trying to synchronously render the component to HTML, retrying until nothing is waiting on pending streams.
do {
// Get all the currently pending keys
const keys = [...pendingKeys];
// Wait for all of them to resolve.
await combineAsArray(
keys.map((key) => (
registeredStreams[key]
.first()
.doAction(({hydration: h}) => {
hydration[key] = h;
pendingKeys.delete(key);
})
))
)
.firstToPromise();
// Remove them from pendingKeys, which may have had more keys added while waiting.
keys.forEach((key) => pendingKeys.delete(key));
// Rethrow any error from the element tree
if (error) {
throw error;
}
} while (pendingKeys.size);
// Now that everything is resolved, synchronously render the html.
const html = render((
<PhaseContext.Provider value={() => SERVER}>
<ServerContext.Provider value={{getStream, onData}}>
{widgetElement}
</ServerContext.Provider>
</PhaseContext.Provider>
));
const id = uuid();
// Return the component HTML and some JavaScript to store props and initial data.
return [
`<div id="${id}"${className ? ` class="${className}"` : ''}>${html}</div>`,
'<script type="text/javascript">',
`Object.assign(["__WIDGET_DATA__","${name}","${id}"].reduce(function(a,b){return a[b]=a[b]||{};},window),${serialize({props: widgetElement.props, hydration}, {isJSON: true})});`,
'</script>',
].join('');
}