react-on-rails
Version:
react-on-rails JavaScript for react_on_rails Ruby gem
263 lines (262 loc) • 9.93 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.reactOnRailsPageLoaded = reactOnRailsPageLoaded;
exports.reactOnRailsComponentLoaded = reactOnRailsComponentLoaded;
exports.clientStartup = clientStartup;
const ReactDOM = require("react-dom");
const createReactOutput_1 = require("./createReactOutput");
const isServerRenderResult_1 = require("./isServerRenderResult");
const reactHydrateOrRender_1 = require("./reactHydrateOrRender");
const reactApis_1 = require("./reactApis");
const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store';
function findContext() {
if (typeof window.ReactOnRails !== 'undefined') {
return window;
}
else if (typeof ReactOnRails !== 'undefined') {
return global;
}
throw new Error(`\
ReactOnRails is undefined in both global and window namespaces.
`);
}
function debugTurbolinks(...msg) {
if (!window) {
return;
}
const context = findContext();
if (context.ReactOnRails && context.ReactOnRails.option('traceTurbolinks')) {
console.log('TURBO:', ...msg);
}
}
function turbolinksInstalled() {
return (typeof Turbolinks !== 'undefined');
}
function turboInstalled() {
const context = findContext();
if (context.ReactOnRails) {
return context.ReactOnRails.option('turbo') === true;
}
return false;
}
function reactOnRailsHtmlElements() {
return document.getElementsByClassName('js-react-on-rails-component');
}
function initializeStore(el, context, railsContext) {
const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || '';
const props = (el.textContent !== null) ? JSON.parse(el.textContent) : {};
const storeGenerator = context.ReactOnRails.getStoreGenerator(name);
const store = storeGenerator(props, railsContext);
context.ReactOnRails.setStore(name, store);
}
function forEachStore(context, railsContext) {
const els = document.querySelectorAll(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`);
for (let i = 0; i < els.length; i += 1) {
initializeStore(els[i], context, railsContext);
}
}
function turbolinksVersion5() {
return (typeof Turbolinks.controller !== 'undefined');
}
function turbolinksSupported() {
return Turbolinks.supported;
}
function delegateToRenderer(componentObj, props, railsContext, domNodeId, trace) {
const { name, component, isRenderer } = componentObj;
if (isRenderer) {
if (trace) {
console.log(`\
DELEGATING TO RENDERER ${name} for dom node with id: ${domNodeId} with props, railsContext:`, props, railsContext);
}
component(props, railsContext, domNodeId);
return true;
}
return false;
}
function domNodeIdForEl(el) {
return el.getAttribute('data-dom-id') || '';
}
/**
* Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or
* delegates to a renderer registered by the user.
*/
function render(el, context, railsContext) {
// This must match lib/react_on_rails/helper.rb
const name = el.getAttribute('data-component-name') || '';
const domNodeId = domNodeIdForEl(el);
const props = (el.textContent !== null) ? JSON.parse(el.textContent) : {};
const trace = el.getAttribute('data-trace') === 'true';
try {
const domNode = document.getElementById(domNodeId);
if (domNode) {
const componentObj = context.ReactOnRails.getComponent(name);
if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) {
return;
}
// Hydrate if available and was server rendered
// @ts-expect-error potentially present if React 18 or greater
const shouldHydrate = !!(ReactDOM.hydrate || ReactDOM.hydrateRoot) && !!domNode.innerHTML;
const reactElementOrRouterResult = (0, createReactOutput_1.default)({
componentObj,
props,
domNodeId,
trace,
railsContext,
shouldHydrate,
});
if ((0, isServerRenderResult_1.isServerRenderHash)(reactElementOrRouterResult)) {
throw new Error(`\
You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)}
You should return a React.Component always for the client side entry point.`);
}
else {
const rootOrElement = (0, reactHydrateOrRender_1.default)(domNode, reactElementOrRouterResult, shouldHydrate);
if (reactApis_1.supportsRootApi) {
context.roots.push(rootOrElement);
}
}
}
}
catch (e) {
console.error(e.message);
e.message = `ReactOnRails encountered an error while rendering component: ${name}. See above error message.`;
throw e;
}
}
function forEachReactOnRailsComponentRender(context, railsContext) {
const els = reactOnRailsHtmlElements();
for (let i = 0; i < els.length; i += 1) {
render(els[i], context, railsContext);
}
}
function parseRailsContext() {
const el = document.getElementById('js-react-on-rails-context');
if (!el) {
// The HTML page will not have an element with ID 'js-react-on-rails-context' if there are no
// react on rails components
return null;
}
if (!el.textContent) {
throw new Error('The HTML element with ID \'js-react-on-rails-context\' has no textContent');
}
return JSON.parse(el.textContent);
}
function reactOnRailsPageLoaded() {
debugTurbolinks('reactOnRailsPageLoaded');
const railsContext = parseRailsContext();
// If no react on rails components
if (!railsContext)
return;
const context = findContext();
if (reactApis_1.supportsRootApi) {
context.roots = [];
}
forEachStore(context, railsContext);
forEachReactOnRailsComponentRender(context, railsContext);
}
function reactOnRailsComponentLoaded(domId) {
debugTurbolinks(`reactOnRailsComponentLoaded ${domId}`);
const railsContext = parseRailsContext();
// If no react on rails components
if (!railsContext)
return;
const context = findContext();
if (reactApis_1.supportsRootApi) {
context.roots = [];
}
const el = document.querySelector(`[data-dom-id=${domId}]`);
if (!el)
return;
render(el, context, railsContext);
}
function unmount(el) {
const domNodeId = domNodeIdForEl(el);
const domNode = document.getElementById(domNodeId);
if (domNode === null) {
return;
}
try {
ReactDOM.unmountComponentAtNode(domNode);
}
catch (e) {
console.info(`Caught error calling unmountComponentAtNode: ${e.message} for domNode`, domNode, e);
}
}
function reactOnRailsPageUnloaded() {
debugTurbolinks('reactOnRailsPageUnloaded');
if (reactApis_1.supportsRootApi) {
const { roots } = findContext();
// If no react on rails components
if (!roots)
return;
for (const root of roots) {
root.unmount();
}
}
else {
const els = reactOnRailsHtmlElements();
for (let i = 0; i < els.length; i += 1) {
unmount(els[i]);
}
}
}
function renderInit() {
// Install listeners when running on the client (browser).
// We must do this check for turbolinks AFTER the document is loaded because we load the
// Webpack bundles first.
if ((!turbolinksInstalled() || !turbolinksSupported()) && !turboInstalled()) {
debugTurbolinks('NOT USING TURBOLINKS: calling reactOnRailsPageLoaded');
reactOnRailsPageLoaded();
return;
}
if (turboInstalled()) {
debugTurbolinks('USING TURBO: document added event listeners ' +
'turbo:before-render and turbo:render.');
document.addEventListener('turbo:before-render', reactOnRailsPageUnloaded);
document.addEventListener('turbo:render', reactOnRailsPageLoaded);
reactOnRailsPageLoaded();
}
else if (turbolinksVersion5()) {
debugTurbolinks('USING TURBOLINKS 5: document added event listeners ' +
'turbolinks:before-render and turbolinks:render.');
document.addEventListener('turbolinks:before-render', reactOnRailsPageUnloaded);
document.addEventListener('turbolinks:render', reactOnRailsPageLoaded);
reactOnRailsPageLoaded();
}
else {
debugTurbolinks('USING TURBOLINKS 2: document added event listeners page:before-unload and ' +
'page:change.');
document.addEventListener('page:before-unload', reactOnRailsPageUnloaded);
document.addEventListener('page:change', reactOnRailsPageLoaded);
}
}
function isWindow(context) {
return context.document !== undefined;
}
function onPageReady(callback) {
if (document.readyState === "complete") {
// When hydrating React from the `readystatechange` listener, Safari skips the `load` event.
// To avoid this, we use a timeout to postpone the callback until after the `load` event.
setTimeout(callback, 0);
}
else {
document.addEventListener("readystatechange", function onReadyStateChange() {
onPageReady(callback);
document.removeEventListener("readystatechange", onReadyStateChange);
});
}
}
function clientStartup(context) {
// Check if server rendering
if (!isWindow(context)) {
return;
}
// Tried with a file local variable, but the install handler gets called twice.
// eslint-disable-next-line no-underscore-dangle
if (context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__) {
return;
}
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
context.__REACT_ON_RAILS_EVENT_HANDLERS_RAN_ONCE__ = true;
onPageReady(renderInit);
}