|  | 
|  | 1 | +import type { ReactElement } from 'react'; | 
|  | 2 | +import type { RegisteredComponent, RailsContext } from './types/index.ts'; | 
|  | 3 | +import ComponentRegistry from './ComponentRegistry.ts'; | 
|  | 4 | +import StoreRegistry from './StoreRegistry.ts'; | 
|  | 5 | +import createReactOutput from './createReactOutput.ts'; | 
|  | 6 | +import reactHydrateOrRender from './reactHydrateOrRender.ts'; | 
|  | 7 | +import { getRailsContext } from './context.ts'; | 
|  | 8 | +import { isServerRenderHash } from './isServerRenderResult.ts'; | 
|  | 9 | + | 
|  | 10 | +const REACT_ON_RAILS_STORE_ATTRIBUTE = 'data-js-react-on-rails-store'; | 
|  | 11 | + | 
|  | 12 | +function initializeStore(el: Element, railsContext: RailsContext): void { | 
|  | 13 | +  const name = el.getAttribute(REACT_ON_RAILS_STORE_ATTRIBUTE) || ''; | 
|  | 14 | +  const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {}; | 
|  | 15 | +  const storeGenerator = StoreRegistry.getStoreGenerator(name); | 
|  | 16 | +  const store = storeGenerator(props, railsContext); | 
|  | 17 | +  StoreRegistry.setStore(name, store); | 
|  | 18 | +} | 
|  | 19 | + | 
|  | 20 | +function forEachStore(railsContext: RailsContext): void { | 
|  | 21 | +  const els = document.querySelectorAll(`[${REACT_ON_RAILS_STORE_ATTRIBUTE}]`); | 
|  | 22 | +  for (let i = 0; i < els.length; i += 1) { | 
|  | 23 | +    initializeStore(els[i], railsContext); | 
|  | 24 | +  } | 
|  | 25 | +} | 
|  | 26 | + | 
|  | 27 | +function domNodeIdForEl(el: Element): string { | 
|  | 28 | +  return el.getAttribute('data-dom-id') || ''; | 
|  | 29 | +} | 
|  | 30 | + | 
|  | 31 | +function delegateToRenderer( | 
|  | 32 | +  componentObj: RegisteredComponent, | 
|  | 33 | +  props: Record<string, unknown>, | 
|  | 34 | +  railsContext: RailsContext, | 
|  | 35 | +  domNodeId: string, | 
|  | 36 | +  trace: boolean, | 
|  | 37 | +): boolean { | 
|  | 38 | +  const { name, component, isRenderer } = componentObj; | 
|  | 39 | + | 
|  | 40 | +  if (isRenderer) { | 
|  | 41 | +    if (trace) { | 
|  | 42 | +      console.log( | 
|  | 43 | +        `\ | 
|  | 44 | +DELEGATING TO RENDERER ${name} for dom node with id: ${domNodeId} with props, railsContext:`, | 
|  | 45 | +        props, | 
|  | 46 | +        railsContext, | 
|  | 47 | +      ); | 
|  | 48 | +    } | 
|  | 49 | + | 
|  | 50 | +    // Call the renderer function with the expected signature | 
|  | 51 | +    (component as (props: Record<string, unknown>, railsContext: RailsContext, domNodeId: string) => void)( | 
|  | 52 | +      props, | 
|  | 53 | +      railsContext, | 
|  | 54 | +      domNodeId, | 
|  | 55 | +    ); | 
|  | 56 | +    return true; | 
|  | 57 | +  } | 
|  | 58 | + | 
|  | 59 | +  return false; | 
|  | 60 | +} | 
|  | 61 | + | 
|  | 62 | +/** | 
|  | 63 | + * Used for client rendering by ReactOnRails. Either calls ReactDOM.hydrate, ReactDOM.render, or | 
|  | 64 | + * delegates to a renderer registered by the user. | 
|  | 65 | + */ | 
|  | 66 | +function renderElement(el: Element, railsContext: RailsContext): void { | 
|  | 67 | +  // This must match lib/react_on_rails/helper.rb | 
|  | 68 | +  const name = el.getAttribute('data-component-name') || ''; | 
|  | 69 | +  const domNodeId = domNodeIdForEl(el); | 
|  | 70 | +  const props = el.textContent !== null ? (JSON.parse(el.textContent) as Record<string, unknown>) : {}; | 
|  | 71 | +  const trace = el.getAttribute('data-trace') === 'true'; | 
|  | 72 | + | 
|  | 73 | +  try { | 
|  | 74 | +    const domNode = document.getElementById(domNodeId); | 
|  | 75 | +    if (domNode) { | 
|  | 76 | +      const componentObj = ComponentRegistry.get(name); | 
|  | 77 | +      if (delegateToRenderer(componentObj, props, railsContext, domNodeId, trace)) { | 
|  | 78 | +        return; | 
|  | 79 | +      } | 
|  | 80 | + | 
|  | 81 | +      // Hydrate if available and was server rendered | 
|  | 82 | +      const shouldHydrate = !!domNode.innerHTML; | 
|  | 83 | + | 
|  | 84 | +      const reactElementOrRouterResult = createReactOutput({ | 
|  | 85 | +        componentObj, | 
|  | 86 | +        props, | 
|  | 87 | +        domNodeId, | 
|  | 88 | +        trace, | 
|  | 89 | +        railsContext, | 
|  | 90 | +        shouldHydrate, | 
|  | 91 | +      }); | 
|  | 92 | + | 
|  | 93 | +      if (isServerRenderHash(reactElementOrRouterResult)) { | 
|  | 94 | +        throw new Error(`\ | 
|  | 95 | +You returned a server side type of react-router error: ${JSON.stringify(reactElementOrRouterResult)} | 
|  | 96 | +You should return a React.Component always for the client side entry point.`); | 
|  | 97 | +      } else { | 
|  | 98 | +        reactHydrateOrRender(domNode, reactElementOrRouterResult as ReactElement, shouldHydrate); | 
|  | 99 | +      } | 
|  | 100 | +    } | 
|  | 101 | +  } catch (e: unknown) { | 
|  | 102 | +    const error = e as Error; | 
|  | 103 | +    console.error(error.message); | 
|  | 104 | +    error.message = `ReactOnRails encountered an error while rendering component: ${name}. See above error message.`; | 
|  | 105 | +    throw error; | 
|  | 106 | +  } | 
|  | 107 | +} | 
|  | 108 | + | 
|  | 109 | +/** | 
|  | 110 | + * Render a single component by its DOM ID. | 
|  | 111 | + * This is the main entry point for rendering individual components. | 
|  | 112 | + */ | 
|  | 113 | +export function renderComponent(domId: string): void { | 
|  | 114 | +  const railsContext = getRailsContext(); | 
|  | 115 | + | 
|  | 116 | +  // If no react on rails context | 
|  | 117 | +  if (!railsContext) return; | 
|  | 118 | + | 
|  | 119 | +  // Initialize stores first | 
|  | 120 | +  forEachStore(railsContext); | 
|  | 121 | + | 
|  | 122 | +  // Find the element with the matching data-dom-id | 
|  | 123 | +  const el = document.querySelector(`[data-dom-id="${domId}"]`); | 
|  | 124 | +  if (!el) return; | 
|  | 125 | + | 
|  | 126 | +  renderElement(el, railsContext); | 
|  | 127 | +} | 
|  | 128 | + | 
|  | 129 | +/** | 
|  | 130 | + * Public API function that can be called to render a component after it has been loaded. | 
|  | 131 | + * This is the function that should be exported and used by the Rails integration. | 
|  | 132 | + */ | 
|  | 133 | +export function reactOnRailsComponentLoaded(domId: string): void { | 
|  | 134 | +  renderComponent(domId); | 
|  | 135 | +} | 
0 commit comments