Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# react-to-webcomponent

`react-to-webcomponent` converts [React](https://reactjs.org/) components to [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)! It lets you share React components as native elements that __don't__ require mounted being through React. The custom element acts as a wrapper for the underlying React component. Use these custom elements with any project that uses HTML even in any framework (vue, svelte, angular, ember, canjs) the same way you would use standard HTML elements.
`react-to-webcomponent` converts [React](https://reactjs.org/) components to [custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)! It lets you share React components as native elements that **don't** require mounted being through React. The custom element acts as a wrapper for the underlying React component. Use these custom elements with any project that uses HTML even in any framework (vue, svelte, angular, ember, canjs) the same way you would use standard HTML elements.

`react-to-webcomponent`:

Expand All @@ -26,10 +26,8 @@ import * as ReactDOM from "react-dom/client"
// When using React 16 and 17 import ReactDom with the commented statement below instead:
// import ReactDom from "react-dom"

const Greeting = ({name}) => {
return (
<h1>Hello, {name}</h1>
)
const Greeting = ({ name }) => {
return <h1>Hello, {name}</h1>
}
```

Expand All @@ -55,7 +53,6 @@ Now we can use `<web-greeting>` like any other HTML element!

Note that by using React 18, `reactToWebComponent` will use the new root API. If your application needs the legacy API, please use React 17


In the above case, the web-greeting custom element is not making use of the `name` property from our `Greeting` component.

## Working with Attributes
Expand All @@ -71,13 +68,11 @@ import PropTypes from "prop-types"
import * as ReactDOM from "react-dom/client"

const Greeting = ({ name }) => {
return (
<h1>Hello, {name}</h1>
)
return <h1>Hello, {name}</h1>
}

Greeting.propTypes = {
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
}
```

Expand All @@ -92,7 +87,7 @@ as follows:
</body>
```

For projects needing more advanced usage of the web components, see our [prgramatic usage and declarative demos](docs/programatic-usage.md).
For projects needing more advanced usage of the web components, see our [programatic usage and declarative demos](docs/programatic-usage.md).

We also have a [complete example using a third party library](docs/complete-example.md).

Expand Down
207 changes: 191 additions & 16 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
component works with.
- `ReactDOM` - A version of ReactDOM (or preact-compat) that the component works with.
- `options` - An optional set of parameters.

- `options.shadow` - Use shadow DOM rather than light DOM.
- `options.dashStyleAttributes` - convert dashed-attirbutes on the web component into camelCase props for the React component
- `options.props` - Array of camelCasedProps to watch as String values or { [camelCasedProps]: String | Number | Boolean | Function | Object | Array | "ref" }

- When specifying Array or Object as the type, the string passed into the attribute must pass `JSON.parse()` requirements.
- When specifying Boolean as the type, "true", "1", "yes", "TRUE", and "t" are mapped to `true`. All strings NOT begining with t, T, 1, y, or Y will be `false`.
- When specifying Function as the type, the string passed into the attribute must be the name of a function on `window` (or `global`). The `this` context of the function will be the instance of the WebComponent / HTMLElement when called.

A new class inheriting from `HTMLElement` is
returned. This class can be directly passed to `customElements.define` as follows:
returned. This class can be directly passed to `customElements.define` as follows:

```js
customElements.define("web-greeting", reactToWebComponent(Greeting, React, ReactDOM))
customElements.define(
"web-greeting",
reactToWebComponent(Greeting, React, ReactDOM),
)
```

Or the class can be defined and used later:
Expand All @@ -32,20 +40,21 @@ document.body.appendChild(myGreeting)
Or the class can be extended:

```js
class WebGreeting extends reactToWebComponent(Greeting, React, ReactDOM)
{
disconnectedCallback(){
super.disconnectedCallback()
// special stuff
}
class WebGreeting extends reactToWebComponent(Greeting, React, ReactDOM) {
disconnectedCallback() {
super.disconnectedCallback()
// special stuff
}
}
customElements.define("web-greeting", WebGreeting)
```

Components can also be implemented using [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) with either `open` or `closed` mode.

```js
const WebGreeting = reactToWebComponent(Greeting, React, ReactDOM, { shadow: 'open' })
const WebGreeting = reactToWebComponent(Greeting, React, ReactDOM, {
shadow: "open",
})

customElements.define("web-greeting", WebGreeting)

Expand All @@ -55,24 +64,190 @@ document.body.appendChild(myGreeting)
var shadowContent = myGreeting.shadowRoot.children[0]
```

Using dashStyleAttributes to convert dashed-attributes into camelCase React props
If propTypes are defined on the underlying React component, dashed-attributes on the webcomponent are converted into the corresponding camelCase React props and the string attribute value is passed in.

```js
class Greeting extends React.Component {
render () {
return <h1>Hello, { this.props.camelCaseName }</h1>
render() {
return <h1>Hello, {this.props.camelCaseName}</h1>
}
}
Greeting.propTypes = {
camelCaseName: PropTypes.string.isRequired
camelCaseName: PropTypes.string.isRequired,
}

customElements.define(
"my-dashed-style-greeting",
reactToWebComponent(Greeting, React, ReactDOM, { dashStyleAttributes: true })
reactToWebComponent(Greeting, React, ReactDOM, {}),
)

document.body.innerHTML = "<my-dashed-style-greeting camel-case-name="Christopher"></my-dashed-style-greetingg>"
document.body.innerHTML =
'<my-dashed-style-greeting camel-case-name="Christopher"></my-dashed-style-greeting>'

console.log(document.body.firstElementChild.innerHTML) // "<h1>Hello, Christopher</h1>"
```

If `options.props` is specified, R2WC will use those props instead of the keys from propTypes. If it's an array, all corresponding kebob-cased attr values will be passed as strings to the underlying React component.

```js
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.camelCaseName}</h1>
}
}

customElements.define(
"my-dashed-style-greeting",
reactToWebComponent(Greeting, React, ReactDOM, {
props: ["camelCaseName"],
}),
)

document.body.innerHTML =
'<my-dashed-style-greeting camel-case-name="Jane"></my-dashed-style-greeting>'

console.log(document.body.firstElementChild.innerHTML) // "<h1>Hello, Jane</h1>"
```

## Typed Props

If `options.props` is an object, the keys are the camelCased React props and the values are any one of the following built in javascript types, or the string "ref":

`String | Number | Boolean | Function | Object | Array | "ref"`

### String | Number | Boolean | Object | Array props

```js
class AttrPropTypeCasting extends React.Component {
render() {
console.log(this.props) // Note
return <h1>Oh my, {this.props.stringProp}</h1>
}
}

customElements.define(
"attr-prop-type-casting",
reactToWebComponent(AttrPropTypeCasting, React, ReactDOM, {
props: {
stringProp: String,
numProp: Number,
floatProp: Number,
trueProp: Boolean,
falseProp: Boolean,
arrayProp: Array,
objProp: Object,
},
}),
)

document.body.innerHTML = `
<attr-prop-type-casting
string-prop="iloveyou"
num-prop="360"
float-prop="0.5"
true-prop="true"
false-prop="false"
array-prop='[true, 100.25, "👽", { "aliens": "welcome" }]'
obj-prop='{ "very": "object", "such": "wow!" }'
></attr-prop-type-casting>
`

/*
console.log(this.props) in the React render function produces this:
{
stringProp: "iloveyou",
numProp: 360,
floatProp: 0.5,
trueProp: true,
falseProp: false,
arrayProp: [true, 100.25, "👽", { aliens: "welcome" }],
objProp: { very: "object", such: "wow!" },
}
*/
```

### Function props

When `Function` is specified as the type, attribute values on the web component will be converted into function references when passed into the underlying React component. The string value of the attribute must be a valid reference to a function on `window` (or on `global`).

```js
function ThemeSelect({ handleClick }) {
return (
<div>
<button onClick={() => handleClick("V")}>V</button>
<button onClick={() => handleClick("Johnny")}>Johnny</button>
<button onClick={() => handleClick("Jane")}>Jane</button>
</div>
)
}

const WebThemeSelect = reactToWebComponent(ThemeSelect, React, ReactDOM, {
props: {
handleClick: Function,
},
})

customElements.define("theme-select", WebThemeSelect)

window.globalFn = function (selected) {
// "this" is the instance of the WebComponent / HTMLElement
const thisIsEl = this === document.querySelector("theme-select")
console.log(thisIsEl, selected)
}

document.body.innerHTML =
"<theme-select handle-click='globalFn'></theme-select>"

setTimeout(
() => document.querySelector("theme-select button:last-child").click(),
0,
)
// ^ calls globalFn, logs: true, "Jane"
```

### "ref" props

If the React component is a class type or has ref props, you can specify attributes as `"ref"` type and `React.createRef()` will automatically happen behind the scenes then attach the reference to the webcomponent instance.

```js
class ComRef extends React.Component {
render() {
return <h1 ref={this.props.h1Ref}>Ref</h1>
}
}

class WebComRef extends reactToWebComponent(ComRef, React, ReactDOM, {
props: {
ref: "ref",
h1Ref: "ref",
},
}) {}

customElements.define("ref-example", WebComRef)

document.body.innerHTML = "<ref-example ref h1-ref></ref-example>"

setTimeout(() => {
const el = document.querySelector("ref-example")

console.log(el.ref.current instanceof ComRef) // logs true

const h1 = el.querySelector("h1")

console.log(el.h1Ref.current === h1) // logs true
}, 0)
```

If your `"ref"` type webcomponent attribute specifies a value, the value will be the name of a global function (like the `Function` prop type above) and be used as a callback reference, recieving the dom element the React component attaches it to as a parameter.

```js
window.globalRefFn = function (el) {
if (!el) {
// if the component rerenders the referenced element, the callback may run with el = null
return
}
console.log(el === this.querySelector("h1")) // logs true
}

body.innerHTML = "<ref-example h1-ref='globalRefFn'></ref-example>"
```
Loading