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....
Embr-Periscope-0.7.4-ReactComponent.modl (2.5 MB)
view.json
{
"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