├── .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 | [![Build Status](https://travis-ci.org/the-road-to-learn-react/use-custom-element.svg?branch=master)](https://travis-ci.org/the-road-to-learn-react/use-custom-element) [![Slack](https://slack-the-road-to-learn-react.wieruch.com/badge.svg)](https://slack-the-road-to-learn-react.wieruch.com/) [![Greenkeeper badge](https://badges.greenkeeper.io/the-road-to-learn-react/use-custom-element.svg)](https://greenkeeper.io/) ![NPM](https://img.shields.io/npm/l/use-custom-element.svg) 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 | --------------------------------------------------------------------------------