@philipoliver/react-to-html-element v1.5.1
react-to-html-element
react-to-html-element turns a React component into a Web Component. To see a practical case of how to use in your existing applications, see the Example section (after reading the documentation of course š)
Sections :
Usages
The React component must declare its properties and their types in the static componentProps attribute (or prop-types), as in the example below.
import React from 'react';
import PropTypes from "prop-types"; // add this line, if you want to declare with PropTypes
const MyButton = ({ someBool, someNumber, someString, someObject, someArray, someSlot, children }) => {
// do anything here with properties
return (
<button disabled={someBool} data-identifier={someNumber} data-extra={someString}>
{children}
</button>
);
}
// Declare as below without using prop-types
MyButton.componentProps = {
someBool: Boolean,
someNumber: Number,
someString: String,
someObject: Object,
someArray: Array,
someSlot: Node,
someFunc: Function,
}
// Or else using prop-types
MyButton.propTypes = {
someBool: PropTypes.bool,
someNumber: PropTypes.number,
someString: PropTypes.string,
someObject: PropTypes.object,
someArray: PropTypes.array,
someSlot: PropTypes.node,
someFunc: PropTypes.func,
}
export default MyButton;Then after declaring the properties, you must register your React components as WebComponent, like this :
import React from 'react';
import * as ReactDOM from "react-dom/client";
import { register } from "react-to-html-element";
import MyButton from "./src/MyButton";
register(MyButton, 'my-button', React, ReactDOM);It's up to you to find the ideal location to register your components and then build them, export them, publish them etc... (you can imagine publishing them to a CDN, npm...)
Use of the web component created :
<html>
<body>
<my-button some-bool="true" some-number="45" some-string="Hello" some-object='{"name": "Will"}' some-array="[1, 2, 3]">
It's a Button
</my-button>
</body>
</html>Extend the WebComponent
You can create an extension of the WebComponent to suit your needs, by adding {returnElement: true} as options. and then it's up to you to define the WebComponent in the DOM.
// ...
import { register } from "react-to-html-element";
import MyButton from "./src/MyButton";
class WCButton extends register(MyButton, null, React, ReactDOM, {returnElement: true})
{
constructor(props) {
super(props);
}
connectedCallback() {
super.connectedCallback();
console.log('Component connected to the DOM');
}
}
customElements.define('my-button', WCButton); // define your component to the DOMSlots
It is possible to add slots in your custom elements. They will be added to the React component props. Here is an example of how to do it :
<my-dialog>
<slot name="header">...</slot>
<slot name="body">...</slot>
<slot name="footer">...</slot>
</my-dialog>
<!-- Or else you can do as below -->
<my-dialog>
<h3 slot="header">...</h3>
<p slot="body">...</p>
<div slot="footer">...</div>
</my-dialog>In your React component :
import React from 'react';
const MyDialog = ({ header, body, footer }) => {
return (
<div>
<div>{header}</div>
<div>{body}</div>
<div>{footer}</div>
</div>
);
}
MyDialog.componentProps = {
header: Node,
body: Node,
footer: Node,
}
export default MyDialog;rootElement
This is a property injected into all registered React components, which corresponds to the instance of the WebComponent rootElement instanceof HTMLElement. For example, it can be used to trigger or intercept JavaScript events :
function MyButton({ someString, rootElement }) {
const buttonClicked = () => {
const event = new CustomEvent('btnClicked', { detail: {identifier: 45}})
rootElement.dispatchEvent(event)
}
return (
<button onClick={buttonClicked}>{someString}</button>
);
}How to listen event :
<html>
<body>
<my-button some-string="Hello"></my-button>
<script>
document.addEventListener('DOMContentLoaded', function () {
let button = document.querySelector('my-button');
button.addEventListener('btnClicked', function (e) {
console.log('identifier clicked : ' + e.detail.identifier);
});
})
</script>
</body>
</html>Usage of Ref
Let's take the example of a React component that has a function that can be called from the DOM, for example validating that an input is valid and returning true if it is or otherwise false.
import React, {forwardRef, useImperativeHandle} from 'react';
const MyInput = forwardRef(({placeholder}, ref) => {
useImperativeHandle(ref, () => ({
isValid: () => {
// put logic here
return true; // or return false
}
}));
return <input placeholder={placeholder} type="text"/>;
});
MyInput.componentProps = {
placeholder: String,
}
export default MyInput;There are 3 important points :
- The component must be wrapped with forwardRef hook.
- Add as second parameter the variable
ref, example(props, ref) or ({props1, prop2}, ref). - Use the useImperativeHandle hook to expose functions outside the component.
After doing that, now here is an example of how to register the component :
// ...
import { register } from "react-to-html-element";
import MyInput from "./src/MyInput";
class WCInput extends register(MyInput, null, React, ReactDOM, {returnElement: true, hasReactRef: true})
{
constructor(props) {
super(props);
}
async isInputValidAsync() {
let ref = await this.getAsyncReactRef();
return ref.isValid(); // call the React component function
}
isInputValid() { // or you can do it like this
return this.getReactRef().isValid(); // call the React component function
}
}
customElements.define('my-input', WCInput); // define your component to the DOM
// call function like this :
let input = document.querySelector('my-input');
let isValid = input.isInputValid();
// or with async (inside async function) :
let input = document.querySelector('my-input');
let isValid = await input.isInputValidAsync();The asynchronous should be used in case the WebComponent may not be ready in the DOM yet, to avoid having undefined
Below is another example of using the ref to sibling the input inside a React component and toggle focus on it :
// ...
const MyInput = forwardRef((props, ref) => {
return <input ref={ref} type="text"/>;
});
class WCInput extends register(MyInput, null, React, ReactDOM, {returnElement: true, hasReactRef: true})
{
// ...
focusInput() {
this.getReactRef().focus(); // call focus method of input
}
}
// clicking on a button activates the focus on the input :
let input = document.querySelector('my-input');
button.addEventListener('click', function () {
input.focusInput();
});Update attributes
After the component has been rendered, you can update the attributes, and the component will be re-rendered :
let button = document.querySelector('my-button');
button.someString = "Good bye";
// or
button.setAttribute("some-string", "Good bye");
button.someArray = [1, 2, 3];
button.someBool = true;
// ...Function attributes need a reference to declared function:
<my-button handle-click="sayHello"></my-button>
<my-button handle-click="Greeting.sayHello"></my-button>
<script>
function sayHello() { ... }
class Greeting {
static sayHello() { ... }
}
</script>
// and retrieve the function inside you React component (props.handleClick)API
The register function has as parameters :
ReactComponentThe React component that needs to be turned into a WebComponent.nameThe name of the desired WebComponent tag.ReactThe version of React that was used to create the components.ReactDOMThe version of ReactDOM that was used to create the components.options: object of optionsdefault = {modeShadow: false, returnElement: false, hasReactRef: false, className: "html-element"}
Conflicts to avoid
There are points to know to allow the proper functioning of this package:
- If you rewrite the
connectedCallbackmethod of the returned component always call the parent by doing:super.connectedCallback(); - The
children,rootElementandrefproperties do not need to be declared, they will be automatically injected into the component. - Do not use these properties in your components:
customcustom-parentcustom-state - If you encounter a problem of FOUC, flickering, glitch (visual problem), add these CSS rules:
[custom]:not([custom-state="hydrated"]) {
visibility: hidden;
}
:not(:defined) {
visibility: hidden;
}Example
See example on codesandbox: https://codesandbox.io/s/react-to-html-element-397stp
To go further, you can imagine for example having a component library in React, which you could export to all your applications, here is an example among hundreds that you could imagine.
Start by creating a React app using create-react-app. You can base yourself on the example in codesandbox, it's exactly the same things except that we will talk about the build and include in your applications.
npx create-react-app my-web-components
cd my-web-componentsYou can remove all the files created by create-react-app inside the src folder, it's up to you to create your tree (structure). You can imagine something like this :
my-web-components/
āāā src/
āāā components/
ā āāā MyButton.jsx
ā āāā MyInput.jsx
ā āāā ...
āāā layouts/
ā āāā Header.jsx
ā āāā Footer.jsx
ā āāā ...
āāā style/
ā āāā index.css
ā āāā my-button.css
ā āāā my-input.css
ā āāā ...
āāā index.js
package.jsonInstall react-to-html-element in your project:
npm install react-to-html-elementThen it's up to you, from the README and the examples in codesandbox, create your components and register them in the src/index.js file!
Then we will use the react-scripts build command which is already installed with create-react-app, being at the root of your project and doing this command :
npm run buildAfter the command completes, there is a build folder created in the root. Inside are the compiled and minified files, for example :
build/static/js/main.e77e15c3.js
build/static/css/main.2e73bf20.css š and if you included cssThese files contain your WebComponents ready to be used anywhere! How to use them :
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="build/static/css/main.2e73bf20.css">
</head>
<body>
<my-button>Button</my-button>
<script src="build/static/js/main.e77e15c3.js"></script> <-- put the JS build here
</body>
</html>