Skip to content

Commit

Permalink
Convert the React bootloader to TypeScript (exercism#5961)
Browse files Browse the repository at this point in the history
* Fix issue with pages not clearing properly

* Remove comment

* Turn react bootloader into typescript, and refactor.

* Add package resolution

* Update imports

* Export `renderComponents`

* Remove resolution

* Fix tests

* Update app/javascript/utils/react-bootloader.tsx

Co-authored-by: Aron Demeter <66035744+dem4ron@users.noreply.github.com>

* Convert back to input validation (exercism#6049)

---------

Co-authored-by: Jeremy Walker <jez.walker@gmail.com>
Co-authored-by: Erik Schierboom <erik_schierboom@hotmail.com>
  • Loading branch information
3 people authored Aug 29, 2023
1 parent 97fd3ed commit 793f01e
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 161 deletions.
159 changes: 0 additions & 159 deletions app/javascript/utils/react-bootloader.jsx

This file was deleted.

184 changes: 184 additions & 0 deletions app/javascript/utils/react-bootloader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Bugsnag from '@bugsnag/js'
import BugsnagPluginReact from '@bugsnag/plugin-react'
import { ExercismTippy } from '../components/misc/ExercismTippy'
import { ReactQueryCacheProvider } from 'react-query'

type ErrorBoundaryType = React.ComponentType<any>

type GeneratorFunc = (data: any, elem: HTMLElement) => JSX.Element
type Mappings = Record<string, GeneratorFunc>

type TurboFrameRenderDetail = {
fetchResponse: {
response: {
headers: {
get: (name: string) => string | null
}
}
}
}

let ErrorBoundary: ErrorBoundaryType = () => <></>

if (process.env.BUGSNAG_API_KEY) {
Bugsnag.start({
apiKey: process.env.BUGSNAG_API_KEY,
releaseStage: process.env.NODE_ENV,
plugins: [new BugsnagPluginReact()],
enabledReleaseStages: ['production'],
collectUserIp: false,
onError: function (event) {
const tag = document.querySelector<HTMLMetaElement>(
'meta[name="user-id"]'
)

if (!tag) {
return true
}

event.setUser(tag.content)
},
})
const reactPlugin = Bugsnag.getPlugin('react')
if (reactPlugin) {
ErrorBoundary = reactPlugin.createErrorBoundary(React)
} else {
throw new Error("Failed to load Bugsnag's react plugin")
}
}

function initEventListeners() {
// This changes any extra things that need changing from the
// turbo frame, such as body class or page title
document.addEventListener('turbo:frame-render', (e) => {
const event = e as CustomEvent<TurboFrameRenderDetail>
const bodyClass = event.detail.fetchResponse.response.headers.get(
'exercism-body-class'
)
if (bodyClass) {
document.body.className = bodyClass
}
})

document.addEventListener('turbo:before-fetch-request', () => {
setTurboStyle('* { cursor:wait !important }')
})

document.addEventListener('turbo:before-render', () => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'auto',
})
setTurboStyle('')
})
}

/********************************************************/
/** Add a loading cursor when a turbo-frame is loading **/
/********************************************************/
const styleElemId = 'turbo-style'
const setTurboStyle = (style: string) => {
let styleElem = document.getElementById(styleElemId)
if (!styleElem) {
styleElem = document.createElement('style')
styleElem.id = styleElemId
document.head.appendChild(styleElem)
}
styleElem.textContent = style
}

export function initReact(mappings: Mappings): void {
const renderThings = () => {
renderComponents(document.body, mappings)
renderTooltips(document.body, mappings)
}

// This adds rendering for all future turbo clicks
document.addEventListener('turbo:load', () => {
renderThings()
})

// This renders if turbo has already finished at the
// point at which this calls. See packs/core.tsx
if (window.turboLoaded) {
renderThings()
}
}

const render = (elem: HTMLElement, component: React.ReactNode) => {
ReactDOM.render(
<React.StrictMode>
<ReactQueryCacheProvider queryCache={window.queryCache}>
<ErrorBoundary>{component}</ErrorBoundary>
</ReactQueryCacheProvider>
</React.StrictMode>,
elem
)
document.addEventListener('turbo:before-frame-render', () => {
ReactDOM.unmountComponentAtNode(elem)
})
}

export function renderComponents(
parentElement: HTMLElement,
mappings: Mappings
): void {
if (!parentElement) {
parentElement = document.body
}
// As getElementsByClassName returns a live collection, it is recommended to use Array.from
// when iterating through it, otherwise the number of elements may change mid-loop.
const elems = Array.from(
parentElement.getElementsByClassName('c-react-component')
)
for (const elem of elems) {
// dataset doesn't exist on type `Element`
if (!(elem instanceof HTMLElement)) continue

const reactId = elem.dataset['reactId']
const reactData = elem.dataset.reactData
const generator = reactId ? mappings[reactId] : null

if (reactId && generator && reactData) {
const data = JSON.parse(reactData)
render(elem, generator(data, elem))
}
}
}

function renderTooltips(parentElement: HTMLElement, mappings: Mappings) {
if (!parentElement) {
parentElement = document.body
}
parentElement
.querySelectorAll('[data-tooltip-type][data-endpoint]')
.forEach((elem) => renderTooltip(mappings, elem as HTMLElement))
}

function renderTooltip(mappings: Mappings, elem: HTMLElement) {
const name = elem.dataset['tooltipType'] + '-tooltip'
const generator = mappings[name]

if (!generator) {
return
}
const component = generator(elem.dataset, elem)

const tooltipElem = document.createElement('span')
elem.insertAdjacentElement('afterend', tooltipElem)

render(
tooltipElem,
<ExercismTippy
interactive={elem.dataset.interactive === 'true'}
renderReactComponents={elem.dataset.renderReactComponents === 'true'}
content={component}
reference={elem}
/>
)
}

initEventListeners()
4 changes: 2 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2149,7 +2149,7 @@
dependencies:
"@types/react" "*"

"@types/react@*":
"@types/react@*", "@types/react@17.0.38", "@types/react@^16":
version "17.0.38"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==
Expand All @@ -2158,7 +2158,7 @@
"@types/scheduler" "*"
csstype "^3.0.2"

"@types/react@^16", "@types/react@^16.9.51":
"@types/react@^16.9.51":
version "16.14.22"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.22.tgz#ee332c031561fa6c5b7fa83d74defce837a2947b"
integrity sha512-4NnkxKDd2UO9SiCckuhCQOCzdO+RtE4Epf1D6eGz3f9jZjiIXOVo+Bk3jqSad+8EOT+LBXwKdkFX0V0F+NFzDQ==
Expand Down

0 comments on commit 793f01e

Please sign in to comment.