├── .babelrc
├── .eslintrc
├── .github
└── FUNDING.yml
├── .gitignore
├── .npmignore
├── .prettierrc
├── .travis.yml
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
└── index.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react"
5 | ]
6 | }
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": ["airbnb", "prettier"],
8 | "plugins": ["prettier", "react-hooks"],
9 | "rules": {
10 | "prettier/prettier": ["error"],
11 | "react-hooks/rules-of-hooks": "error",
12 | "react-hooks/exhaustive-deps": "warn"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: rwieruch
4 | patreon: # rwieruch
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with a single custom sponsorship URL
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logfile
2 |
3 | .env
4 |
5 | node_modules/
6 | lib/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src/
2 | test/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "trailingComma": "all",
4 | "singleQuote": true,
5 | "printWidth": 70,
6 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - stable
5 |
6 | install:
7 | - npm install
8 |
9 | script:
10 | - npm run test
11 |
12 | after_script:
13 | - npm run coverage
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Robin Wieruch
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # useCustomElement React Hook
2 |
3 | [](https://travis-ci.org/the-road-to-learn-react/use-custom-element) [](https://slack-the-road-to-learn-react.wieruch.com/) [](https://greenkeeper.io/) 
4 |
5 | Custom hook to bridge Custom Elements (Web Components) to React.
6 |
7 | * [Learn how to build Web Components.](https://www.robinwieruch.de/web-components-tutorial)
8 | * [Learn how to use Web Components in React.](https://www.robinwieruch.de/react-web-components)
9 |
10 | ## Installation
11 |
12 | `npm install use-custom-element`
13 |
14 | ## Usage
15 |
16 | In this scenario, we are using a specific [Dropdown Web Component](https://github.com/rwieruch/web-components-dropdown) as a React Dropdown Component. By using the custom React hook, we can provide all props in the right format to the custom element and register all event listeners (e.g. onChange from `props`) behind the scenes.
17 |
18 | ```
19 | import React from 'react';
20 |
21 | // Web Component Use Case
22 | // install via npm install road-dropdown
23 | import 'road-dropdown';
24 |
25 | import useCustomElement from 'use-custom-element';
26 |
27 | const Dropdown = props => {
28 | const [customElementProps, ref] = useCustomElement(props);
29 |
30 | return ;
31 | };
32 | ```
33 |
34 | Afterward, the Dropdown component can be used:
35 |
36 | ```
37 | const props = {
38 | label: 'Label',
39 | option: 'option1',
40 | options: {
41 | option1: { label: 'Option 1' },
42 | option2: { label: 'Option 2' },
43 | },
44 | onChange: (value) => console.log(value),
45 | };
46 |
47 | return ;
48 | ```
49 |
50 | ### Custom Mapping
51 |
52 | You can also define a custom mapping:
53 |
54 | ```
55 | import React from 'react';
56 |
57 | // Web Component Use Case
58 | // install via npm install road-dropdown
59 | import 'road-dropdown';
60 |
61 | import useCustomElement from 'use-custom-element';
62 |
63 | const Dropdown = props => {
64 | const [customElementProps, ref] = useCustomElement(props, {
65 | option: 'selected',
66 | onChange: 'click',
67 | });
68 |
69 | console.log(customElementProps);
70 |
71 | // label: "Label"
72 | // options: "{"option1":{"label":"Option 1"},"option2":{"label":"Option 2"}}"
73 | // selected: "option1"
74 |
75 | // and "onChange" mapped to "click" event for the event listener
76 |
77 | return ;
78 | };
79 | ```
80 |
81 | ## Contribute
82 |
83 | * `git clone git@github.com:the-road-to-learn-react/use-custom-element.git`
84 | * `cd use-custom-element`
85 | * `npm install`
86 | * `npm run test`
87 |
88 | ### More
89 |
90 | * [Publishing a Node Package to NPM](https://www.robinwieruch.de/publish-npm-package-node/)
91 | * [Node.js Testing Setup](https://www.robinwieruch.de/node-js-testing-mocha-chai/)
92 | * [React Testing Setup](https://www.robinwieruch.de/react-testing-tutorial/)
93 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "use-custom-element",
3 | "version": "1.0.5",
4 | "description": "",
5 | "main": "lib/index.js",
6 | "scripts": {
7 | "build": "webpack --config ./webpack.config.js --mode=production",
8 | "prepare": "npm run build",
9 | "test": "echo \"Error: no test specified\" && exit 0"
10 | },
11 | "keywords": [],
12 | "author": "“Robin (https://www.robinwieruch.de)",
13 | "license": "MIT",
14 | "peerDependencies": {
15 | "react": "^16.8.6",
16 | "react-dom": "^16.8.6"
17 | },
18 | "devDependencies": {
19 | "@babel/cli": "^7.4.4",
20 | "@babel/core": "^7.2.2",
21 | "@babel/node": "^7.2.2",
22 | "@babel/preset-env": "^7.2.3",
23 | "@babel/preset-react": "^7.0.0",
24 | "@babel/register": "^7.4.4",
25 | "babel-eslint": "^10.0.2",
26 | "babel-loader": "^8.0.5",
27 | "eslint": "^5.16.0",
28 | "eslint-config-airbnb": "^17.1.0",
29 | "eslint-config-prettier": "^6.0.0",
30 | "eslint-loader": "^2.1.2",
31 | "eslint-plugin-import": "^2.18.0",
32 | "eslint-plugin-jsx-a11y": "^6.2.1",
33 | "eslint-plugin-prettier": "^3.1.0",
34 | "eslint-plugin-react": "^7.14.2",
35 | "eslint-plugin-react-hooks": "^1.6.1",
36 | "prettier": "^1.18.2",
37 | "react": "^16.8.6",
38 | "react-dom": "^16.8.6",
39 | "webpack": "^4.31.0",
40 | "webpack-cli": "^3.3.2"
41 | },
42 | "repository": {
43 | "type": "git",
44 | "url": "git+https://github.com/the-road-to-learn-react/use-custom-element.git"
45 | },
46 | "bugs": {
47 | "url": "https://github.com/the-road-to-learn-react/use-custom-element/issues"
48 | },
49 | "homepage": "https://github.com/the-road-to-learn-react/use-custom-element#readme",
50 | "dependencies": {}
51 | }
52 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const useCustomElement = (props, customMapping = {}) => {
4 | const ref = React.createRef();
5 |
6 | React.useLayoutEffect(() => {
7 | const { current } = ref;
8 |
9 | let fns;
10 |
11 | if (current) {
12 | fns = Object.keys(props)
13 | .filter(key => props[key] instanceof Function)
14 | .map(key => ({
15 | key: customMapping[key] || key,
16 | fn: customEvent =>
17 | props[key](customEvent.detail, customEvent),
18 | }));
19 |
20 | fns.forEach(({ key, fn }) => current.addEventListener(key, fn));
21 | }
22 |
23 | return () => {
24 | if (current) {
25 | fns.forEach(({ key, fn }) =>
26 | current.removeEventListener(key, fn),
27 | );
28 | }
29 | };
30 | }, [customMapping, props, ref]);
31 |
32 | const customElementProps = Object.keys(props)
33 | .filter(key => !(props[key] instanceof Function))
34 | .reduce((acc, key) => {
35 | const prop = props[key];
36 |
37 | const computedKey = customMapping[key] || key;
38 |
39 | if (prop instanceof Object || prop instanceof Array) {
40 | return { ...acc, [computedKey]: JSON.stringify(prop) };
41 | }
42 |
43 | return { ...acc, [computedKey]: prop };
44 | }, {});
45 |
46 | return [customElementProps, ref];
47 | };
48 |
49 | export default useCustomElement;
50 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | entry: './src/index.js',
5 | output: {
6 | path: path.resolve(__dirname, 'lib'),
7 | filename: 'index.js',
8 | library: 'use-custom-element',
9 | libraryTarget: 'umd',
10 | },
11 | externals: {
12 | react: 'react',
13 | 'react-dom': 'react-dom',
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.(js|jsx)$/,
19 | exclude: /node_modules/,
20 | use: ['babel-loader', 'eslint-loader'],
21 | },
22 | ],
23 | },
24 | };
25 |
--------------------------------------------------------------------------------