# Three Javascript injection

**URL:** <https://forum.inductiveautomation.com/t/three-javascript-injection/106832>\
**Category:** Ignition\
**Tags:** perspective\
**Created:** [May 29, 2025, 7:00pm UTC](https://forum.inductiveautomation.com/t/three-javascript-injection/106832 "2025-05-29T19:00:33Z")\
**Posts on this page:** 1\
**Showing post:** 7

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [June 3, 2025, 9:39pm UTC](https://forum.inductiveautomation.com/t/three-javascript-injection/106832/7 "2025-06-03T21:39:10Z")

</div>

Here's a super untested component that will render user-supplied JavaScript as a React Function component. It uses Babel to transpile JSX on the client-side, no idea yet what the performance implications are.

Props are passed in like you'd expect. You also currently have access to the Bable `TransformOptions` through `options.babel`

I spent more time on the component icons than the component itself, so I'm sure this has lifecycle problems somewhere 🤷‍♂️.

The attached view shows an example of loading Three.js from a CDN.

I'm not sure I want to go down the road of including `Three.js` in Periscope... there is simply an endless amount of JavaScript libraries that someone may want to include, and I want to be conscious of Periscope's bundle size. However, I do recognize that loading from a CDN is not feasible for most Ignition installations. I think it's time for inclusion of the `web-library` folder feature.

I don't currently have much time to allocate to this component, which is why I'm shadow dropping it in its current state. Any testing/feedback would be appreciated.

P.S. Still wishing for a way to interact with [NodeEditor.createValueEditor](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.33/com/inductiveautomation/ignition/client/jsonedit/NodeEditor.html).... 🧞 🪔 💭💭

[Embr-Periscope-0.7.4-ReactComponent.modl](https://forum.inductiveautomation.com/uploads/short-url/gxLjm3P7QUx5RtpgsyL7GrtD2iP.modl) (2.5 MB)

> **view.json**
>
> ```python
> {
> "custom": {},
> "params": {},
> "props": {
> "defaultSize": {
> "height": 747,
> "width": 1399
> }
> },
> "root": {
> "children": [
> {
> "meta": {
> "name": "React_1"
> },
> "position": {
> "height": 694,
> "width": 754,
> "y": -0.5
> },
> "props": {
> "component": "() \u003d\u003e {\n const containerRef \u003d React.useRef(null);\n\n React.useEffect(() \u003d\u003e {\n let renderer, camera, scene, cube, frameId;\n let resizeObserver;\n\n const loadAndInit \u003d async () \u003d\u003e {\n const THREE \u003d await import(\u0027https://cdn.jsdelivr.net/npm/three@0.161.0/build/three.module.js\u0027);\n\n const {\n Scene,\n PerspectiveCamera,\n WebGLRenderer,\n BoxGeometry,\n MeshBasicMaterial,\n Mesh,\n Color,\n } \u003d THREE;\n\n scene \u003d new Scene();\n scene.background \u003d new Color(0x202030);\n\n const container \u003d containerRef.current;\n const width \u003d container.clientWidth;\n const height \u003d container.clientHeight;\n\n camera \u003d new PerspectiveCamera(75, width / height, 0.1, 1000);\n camera.position.z \u003d 3;\n\n renderer \u003d new WebGLRenderer({ antialias: true });\n renderer.setSize(width, height);\n container.appendChild(renderer.domElement);\n\n const geometry \u003d new BoxGeometry();\n const material \u003d new MeshBasicMaterial({ color: 0x44aa88, wireframe: true });\n cube \u003d new Mesh(geometry, material);\n scene.add(cube);\n\n const animate \u003d () \u003d\u003e {\n cube.rotation.x +\u003d 0.01;\n cube.rotation.y +\u003d 0.01;\n renderer.render(scene, camera);\n frameId \u003d requestAnimationFrame(animate);\n };\n\n animate();\n\n const handleResize \u003d () \u003d\u003e {\n if (!renderer || !camera) return;\n const newWidth \u003d container.clientWidth;\n const newHeight \u003d container.clientHeight;\n camera.aspect \u003d newWidth / newHeight;\n camera.updateProjectionMatrix();\n renderer.setSize(newWidth, newHeight);\n };\n\n resizeObserver \u003d new ResizeObserver(handleResize);\n resizeObserver.observe(container);\n };\n\n loadAndInit().catch(console.error);\n\n return () \u003d\u003e {\n cancelAnimationFrame(frameId);\n if (resizeObserver) {\n resizeObserver.disconnect();\n }\n if (renderer?.domElement \u0026\u0026 containerRef.current?.contains(renderer.domElement)) {\n containerRef.current.removeChild(renderer.domElement);\n }\n };\n }, []);\n\n return (\n \u003cdiv\n ref\u003d{containerRef}\n style\u003d{{\n width: \u0027100%\u0027,\n height: \u0027100%\u0027,\n overflow: \u0027hidden\u0027,\n margin: 0,\n padding: 0,\n position: \u0027relative\u0027,\n }}\n /\u003e\n );\n}",
> "options": {
> "babel": {
> "presets": [
> "react"
> ]
> }
> },
> "props": {}
> },
> "type": "embr.periscope.embedding.react"
> },
> {
> "meta": {
> "name": "React"
> },
> "position": {
> "height": 32,
> "width": 300,
> "x": 750,
> "y": -0.5
> },
> "props": {
> "component": "(props) \u003d\u003e \u003cdiv\u003e{props.text}\u003c/div\u003e",
> "options": {
> "babel": {
> "presets": [
> "react"
> ]
> }
> },
> "props": {
> "text": "Hello World!"
> }
> },
> "type": "embr.periscope.embedding.react"
> },
> {
> "meta": {
> "name": "React_0"
> },
> "position": {
> "height": 503,
> "width": 300,
> "x": 770,
> "y": 188
> },
> "props": {
> "component": "(props) \u003d\u003e {\n const styles \u003d {\n container: {\n minHeight: \u0027auto\u0027,\n height: \u0027100%\u0027,\n display: \u0027flex\u0027,\n alignItems: \u0027center\u0027,\n justifyContent: \u0027center\u0027,\n backgroundColor: \u0027transparent\u0027\n },\n form: {\n backgroundColor: \u0027#ffffff\u0027,\n padding: \u00272rem\u0027,\n borderRadius: \u00271rem\u0027,\n boxShadow: \u00270 10px 25px rgba(0, 0, 0, 0.1)\u0027,\n width: \u0027100%\u0027,\n maxWidth: \u0027400px\u0027,\n },\n title: {\n fontSize: \u00271.5rem\u0027,\n fontWeight: \u0027bold\u0027,\n marginBottom: \u00271.5rem\u0027,\n color: \u0027#1f2937\u0027,\n textAlign: \u0027center\u0027,\n },\n label: {\n display: \u0027block\u0027,\n color: \u0027#4b5563\u0027,\n marginBottom: \u00270.5rem\u0027,\n },\n input: {\n width: \u0027100%\u0027,\n padding: \u00270.5rem 1rem\u0027,\n border: \u00271px solid #d1d5db\u0027,\n borderRadius: \u00270.5rem\u0027,\n outline: \u0027none\u0027,\n marginBottom: \u00271.5rem\u0027,\n },\n button: {\n width: \u0027100%\u0027,\n backgroundColor: \u0027#2563eb\u0027,\n color: \u0027#ffffff\u0027,\n padding: \u00270.5rem 1rem\u0027,\n borderRadius: \u00270.5rem\u0027,\n border: \u0027none\u0027,\n cursor: \u0027pointer\u0027,\n transition: \u0027background-color 0.2s ease\u0027,\n },\n footerText: {\n textAlign: \u0027center\u0027,\n fontSize: \u00270.875rem\u0027,\n color: \u0027#6b7280\u0027,\n marginTop: \u00271rem\u0027,\n },\n link: {\n color: \u0027#2563eb\u0027,\n textDecoration: \u0027none\u0027,\n },\n };\n\n const handleClick \u003d (message) \u003d\u003e (event) \u003d\u003e {\n event.preventDefault();\n perspective.sendMessage(message, {}, \u0027page\u0027);\n };\n\n return (\n \u003cdiv style\u003d{styles.container}\u003e\n \u003cform style\u003d{styles.form}\u003e\n \u003ch2 style\u003d{styles.title}\u003e{props.title}\u003c/h2\u003e\n\n \u003clabel htmlFor\u003d\"email\" style\u003d{styles.label}\u003eEmail\u003c/label\u003e\n \u003cinput\n type\u003d\"email\"\n id\u003d\"email\"\n placeholder\u003d\"you@example.com\"\n style\u003d{styles.input}\n /\u003e\n\n \u003clabel htmlFor\u003d\"password\" style\u003d{styles.label}\u003ePassword\u003c/label\u003e\n \u003cinput\n type\u003d\"password\"\n id\u003d\"password\"\n placeholder\u003d\"••••••••\"\n style\u003d{styles.input}\n /\u003e\n\n \u003cbutton type\u003d\"button\" onClick\u003d{handleClick(\u0027loginClicked\u0027)} style\u003d{styles.button}\u003e\n Log In\n \u003c/button\u003e\n\n \u003cp style\u003d{styles.footerText}\u003e\n Don’t have an account?{\u0027 \u0027}\n \u003ca href\u003d\"#\" onClick\u003d{handleClick(\u0027signupClicked\u0027)} style\u003d{styles.link}\u003eSign up\u003c/a\u003e\n \u003c/p\u003e\n \u003c/form\u003e\n \u003c/div\u003e\n );\n}",
> "options": {
> "babel": {
> "presets": [
> "react"
> ]
> }
> },
> "props": {
> "title": "New Title!!"
> },
> "style": {
> "overflow": "visible"
> }
> },
> "scripts": {
> "customMethods": [],
> "extensionFunctions": null,
> "messageHandlers": [
> {
> "messageType": "loginClicked",
> "pageScope": true,
> "script": "\tsystem.perspective.print(\u0027Login Clicked!\u0027)",
> "sessionScope": false,
> "viewScope": false
> },
> {
> "messageType": "signupClicked",
> "pageScope": true,
> "script": "\tsystem.perspective.print(\u0027Signup Clicked!\u0027)",
> "sessionScope": false,
> "viewScope": false
> }
> ]
> },
> "type": "embr.periscope.embedding.react"
> }
> ],
> "meta": {
> "name": "root"
> },
> "type": "ia.container.coord"
> }
> }
> 
> ```

Edit: Here's the branch - [GitHub - mussonindustrial/embr at feat/periscope/react-component](https://github.com/mussonindustrial/embr/tree/feat/periscope/react-component)

---

_[View the full topic](https://forum.inductiveautomation.com/t/three-javascript-injection/106832)._
