├── .npmignore ├── src ├── global-style.js ├── colors.js ├── basic-atom.js ├── button.js └── index.js ├── .gitignore ├── .prettierrc ├── .travis.yml ├── webpack.dev.config.js ├── webpack.prod.config.js ├── package.json ├── dist └── index.html ├── LICENSE └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | src/ -------------------------------------------------------------------------------- /src/global-style.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | :host { 3 | font-family: sans-serif; 4 | } 5 | `; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log* 2 | yarn-debug.log* 3 | yarn-error.log* 4 | 5 | node_modules 6 | lib 7 | 8 | .npmrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 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 test -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.js', 3 | output: { 4 | path: __dirname + '/dist', 5 | publicPath: '/', 6 | filename: 'bundle.js' 7 | }, 8 | devServer: { 9 | contentBase: './dist' 10 | } 11 | }; -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/index.js', 3 | output: { 4 | path: __dirname + '/lib', 5 | filename: 'bundle.js', 6 | library: 'road-dropdown', 7 | libraryTarget: 'umd', 8 | }, 9 | devtool: 'source-map', 10 | }; 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "road-dropdown", 3 | "version": "1.0.8", 4 | "description": "Dropdown with Web Components", 5 | "main": "lib/bundle.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config ./webpack.dev.config.js --mode development", 8 | "build": "webpack --config ./webpack.prod.config.js --mode=production", 9 | "prepare": "npm run build", 10 | "test": "echo \"Error: no test specified\" && exit 0" 11 | }, 12 | "keywords": [], 13 | "author": "Robin Wieruch (https://www.robinwieruch.de)", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "webpack": "^4.32.2", 17 | "webpack-cli": "^3.3.2", 18 | "webpack-dev-server": "^3.5.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dropdown with Web Components 5 | 6 | 7 | 12 | 13 | 14 | 15 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | export const trwhite = { hex: '#ffffff', rgb: '255,255,255' }; 2 | export const trblue0 = { hex: '#b9dcf7', rgb: '185,220,247' }; 3 | export const trblue1 = { hex: '#74b9ef', rgb: '116,185,239' }; 4 | export const trblue2 = { hex: '#51a7eb', rgb: '81,167,235' }; 5 | export const trblue3 = { hex: '#177AC9', rgb: '23,122,201' }; 6 | export const trblue4 = { hex: '#285172', rgb: '40,81,114' }; 7 | export const trblue5 = { hex: '#142939', rgb: '20,41,57' }; 8 | export const trblue6 = { hex: '#899fb2', rgb: '137,159,178' }; 9 | export const trgrey1 = { hex: '#e7ecf0', rgb: '231,236,240' }; 10 | export const trgrey2 = { hex: '#a1a1a1', rgb: '161,161,161' }; 11 | export const trgrey3 = { hex: '#363636', rgb: '54,54,54' }; 12 | export const trgrey4 = { hex: '#131313', rgb: '19,19,19' }; 13 | export const trgrey5 = { hex: '#9a9a9a', rgb: '154,154,154' }; 14 | export const trgrey6 = { hex: '#595959', rgb: '89,89,89' }; 15 | export const trgrey7 = { hex: '#707070', rgb: '112,112,112' }; 16 | export const trgrey8 = { hex: '#c4c4c4', rgb: '196,196,196' }; 17 | export const trblack = { hex: '#000000', rgb: '0,0,0' }; 18 | export const trred1 = { hex: '#ff1e00', rgb: '255,30,0' }; 19 | export const trred2 = { hex: '#cc292d', rgb: '204,41,45' }; 20 | export const trgreen = { hex: '#2f8f28', rgb: '47,143,40' }; 21 | export const tryellow = { hex: '#f0cf2b', rgb: '240,207,43' }; 22 | 23 | -------------------------------------------------------------------------------- /src/basic-atom.js: -------------------------------------------------------------------------------- 1 | import * as COLORS from './colors.js'; 2 | 3 | export default ` 4 | .basic-atom { 5 | box-sizing: border-box; 6 | position: relative; 7 | border: 1px solid ${COLORS.trgrey2.hex}; 8 | background: ${COLORS.trwhite.hex}; 9 | box-shadow: 0 2px 4px 0 rgba(${ 10 | COLORS.trblack.rgb 11 | }, 0.05), 0 2px 8px 0 rgba(${COLORS.trgrey2.rgb}, 0.4); 12 | color: ${COLORS.trgrey3.hex}; 13 | cursor: pointer; 14 | } 15 | 16 | .basic-atom:focus { 17 | border: 1px solid ${COLORS.trblue2.hex}; 18 | box-shadow: 0 2px 4px 0 rgba(${ 19 | COLORS.trgrey2.rgb 20 | }, 0.4), 0 2px 8px 0 rgba(${ 21 | COLORS.trblack.rgb 22 | }, 0.05), inset 0 0 0 1px ${COLORS.trblue2.hex}; 23 | } 24 | 25 | .basic-atom.selected { 26 | border-color: ${COLORS.trblue2.hex}; 27 | background: rgba(${COLORS.trblue0.rgb}, 0.4); 28 | box-shadow: 0 2px 8px 0 rgba(${COLORS.trblue2.rgb}, 0.25); 29 | color: ${COLORS.trblue4.hex}; 30 | } 31 | 32 | .basic-atom.selected:before { 33 | content: ''; 34 | position: absolute; 35 | top: 0; 36 | right: 0; 37 | bottom: 0; 38 | left: 0; 39 | z-index: 1; 40 | box-shadow: 0 2px 8px 0 rgba(${COLORS.trblue2.rgb}, 0.25); 41 | } 42 | 43 | .basic-atom.selected:hover, 44 | .basic-atom.selected:focus { 45 | box-shadow: 0 2px 8px 0 rgba(${COLORS.trblue2.rgb}, 0.25); 46 | } 47 | 48 | .basic-atom:hover { 49 | border-color: ${COLORS.trblue2.hex}; 50 | color: ${COLORS.trblue3.hex}; 51 | } 52 | 53 | .basic-atom:active { 54 | border: 1px solid ${COLORS.trblue2.hex}; 55 | background: rgba(${COLORS.trblue2.rgb}, 0.1); 56 | color: ${COLORS.trblue3.hex}; 57 | box-shadow: none; 58 | } 59 | 60 | .basic-atom:disabled { 61 | border-color: rgba(${COLORS.trgrey2.rgb}, 0.25); 62 | box-shadow: none; 63 | color: rgba(${COLORS.trgrey3.rgb}, 0.75); 64 | font-weight: normal; 65 | pointer-events: none; 66 | 67 | } 68 | 69 | .basic-atom:disabled .icon { 70 | color: ${COLORS.trgrey5.hex}; 71 | } 72 | `; 73 | -------------------------------------------------------------------------------- /src/button.js: -------------------------------------------------------------------------------- 1 | import globalStyle from './global-style.js'; 2 | import basicAtom from './basic-atom.js'; 3 | 4 | const template = document.createElement('template'); 5 | 6 | template.innerHTML = ` 7 | 65 | 66 |
67 | 68 |
69 | `; 70 | 71 | class Button extends HTMLElement { 72 | constructor() { 73 | super(); 74 | 75 | this._shadowRoot = this.attachShadow({ mode: 'open' }); 76 | this._shadowRoot.appendChild(template.content.cloneNode(true)); 77 | 78 | this.$container = this._shadowRoot.querySelector('.container'); 79 | this.$button = this._shadowRoot.querySelector('button'); 80 | 81 | this.$button.addEventListener('click', () => { 82 | this.dispatchEvent( 83 | new CustomEvent('onClick', { 84 | detail: 'Hello from within the Custom Element', 85 | }) 86 | ); 87 | }); 88 | } 89 | 90 | connectedCallback() { 91 | if (this.hasAttribute('as-atom')) { 92 | this.updateAsAtom(); 93 | } 94 | } 95 | 96 | updateAsAtom() { 97 | this.$container.style.padding = '0px'; 98 | } 99 | 100 | get label() { 101 | return this.getAttribute('label'); 102 | } 103 | 104 | set label(value) { 105 | this.setAttribute('label', value); 106 | } 107 | 108 | static get observedAttributes() { 109 | return ['label']; 110 | } 111 | 112 | attributeChangedCallback(name, oldVal, newVal) { 113 | this.render(); 114 | } 115 | 116 | render() { 117 | this.$button.innerHTML = this.label; 118 | } 119 | } 120 | 121 | window.customElements.define('road-button', Button); 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dropdown with Web Components 2 | 3 | [![Build Status](https://travis-ci.org/rwieruch/web-components-dropdown.svg?branch=master)](https://travis-ci.org/rwieruch/web-components-dropdown) [![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/rwieruch/web-components-dropdown.svg)](https://greenkeeper.io/) 4 | 5 | Dropdown as Web Componment. 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 | ## How to use: 11 | 12 | Install: `npm install road-dropdown` 13 | 14 | ### Vanilla JS + HTML 15 | 16 | Usage with attributes only except for function: 17 | 18 | ``` 19 | // HTML 20 | 21 | 26 | ``` 27 | 28 | ``` 29 | // JavaScript 30 | 31 | document 32 | .querySelector('road-dropdown') 33 | .addEventListener('onChange', value => console.log(value)); 34 | ``` 35 | 36 | Alternative with properties for object/array: 37 | 38 | ``` 39 | // HTML 40 | 41 | 45 | ``` 46 | 47 | ``` 48 | // JavaScript 49 | 50 | document.querySelector('road-dropdown').options = { 51 | option1: { label: 'Option 1' }, 52 | option2: { label: 'Option 2' }, 53 | }; 54 | 55 | document 56 | .querySelector('road-dropdown') 57 | .addEventListener('onChange', value => console.log(value)); 58 | ``` 59 | 60 | ### React 61 | 62 | ``` 63 | import React from 'react'; 64 | 65 | // npm install road-dropdown 66 | import 'road-dropdown'; 67 | 68 | const Dropdown = ({ label, option, options, onChange }) => { 69 | const ref = React.createRef(); 70 | 71 | React.useLayoutEffect(() => { 72 | const handleChange = customEvent => onChange(customEvent.detail); 73 | 74 | ref.current.addEventListener('onChange', handleChange); 75 | 76 | return () => ref.current.removeEventListener('onChange', handleChange); 77 | }, []); 78 | 79 | return ( 80 | 87 | ); 88 | }; 89 | 90 | export default Dropdown; 91 | ``` 92 | 93 | ### React with Hook 94 | 95 | Hook to use Web Components in React Components: [use-custom-element](https://github.com/the-road-to-learn-react/use-custom-element). 96 | 97 | ``` 98 | import React from 'react'; 99 | 100 | // npm install road-dropdown 101 | import 'road-dropdown'; 102 | 103 | // npm install use-custom-element 104 | import useCustomElement from 'use-custom-element'; 105 | 106 | const Dropdown = props => { 107 | const [customElementProps, ref] = useCustomElement(props); 108 | 109 | return ; 110 | }; 111 | ``` 112 | 113 | ## Contribution 114 | 115 | * `git clone git@github.com:rwieruch/web-components-dropdown.git` 116 | * cd web-components-dropdown 117 | * npm install 118 | * npm start 119 | * visit `http://localhost:8080` 120 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import globalStyle from './global-style.js'; 2 | import basicAtom from './basic-atom.js'; 3 | import * as COLORS from './colors.js'; 4 | 5 | import './button.js'; 6 | 7 | const template = document.createElement('template'); 8 | 9 | template.innerHTML = ` 10 | 105 | 106 | 115 | `; 116 | 117 | class Dropdown extends HTMLElement { 118 | constructor() { 119 | super(); 120 | 121 | this._sR = this.attachShadow({ mode: 'open' }); 122 | this._sR.appendChild(template.content.cloneNode(true)); 123 | 124 | this.open = false; 125 | 126 | this.$label = this._sR.querySelector('.label'); 127 | this.$button = this._sR.querySelector('road-button'); 128 | this.$dropdown = this._sR.querySelector('.dropdown'); 129 | this.$dropdownList = this._sR.querySelector('.dropdown-list'); 130 | 131 | this.$button.addEventListener( 132 | 'onClick', 133 | this.toggleOpen.bind(this) 134 | ); 135 | } 136 | 137 | static get observedAttributes() { 138 | return ['label', 'option', 'options']; 139 | } 140 | 141 | get label() { 142 | return this.getAttribute('label'); 143 | } 144 | 145 | set label(value) { 146 | this.setAttribute('label', value); 147 | } 148 | 149 | get option() { 150 | return this.getAttribute('option'); 151 | } 152 | 153 | set option(value) { 154 | this.setAttribute('option', value); 155 | } 156 | 157 | get options() { 158 | return JSON.parse(this.getAttribute('options')); 159 | } 160 | 161 | set options(value) { 162 | this.setAttribute('options', JSON.stringify(value)); 163 | } 164 | 165 | toggleOpen(event) { 166 | this.open = !this.open; 167 | 168 | this.open 169 | ? this.$dropdown.classList.add('open') 170 | : this.$dropdown.classList.remove('open'); 171 | } 172 | 173 | static get observedAttributes() { 174 | return ['label', 'option', 'options']; 175 | } 176 | 177 | attributeChangedCallback(name, oldVal, newVal) { 178 | this.render(); 179 | } 180 | 181 | render() { 182 | this.$label.innerHTML = this.label; 183 | 184 | if (this.options) { 185 | this.$button.setAttribute( 186 | 'label', 187 | this.options[this.option].label 188 | ); 189 | } 190 | 191 | this.$dropdownList.innerHTML = ''; 192 | 193 | Object.keys(this.options || {}).forEach(key => { 194 | let option = this.options[key]; 195 | let $option = document.createElement('li'); 196 | 197 | $option.innerHTML = option.label; 198 | $option.classList.add('basic-atom'); 199 | 200 | if (this.option && this.option === key) { 201 | $option.classList.add('selected'); 202 | } 203 | 204 | $option.addEventListener('click', () => { 205 | this.option = key; 206 | 207 | this.toggleOpen(); 208 | 209 | this.dispatchEvent( 210 | new CustomEvent('onChange', { detail: key }) 211 | ); 212 | 213 | this.render(); 214 | }); 215 | 216 | this.$dropdownList.appendChild($option); 217 | }); 218 | } 219 | } 220 | 221 | window.customElements.define('road-dropdown', Dropdown); 222 | --------------------------------------------------------------------------------