UNPKG

react-on-rails

Version:

react-on-rails JavaScript for react_on_rails Ruby gem

263 lines (262 loc) 9.93 kB
"use strict"; 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); }