├── .gitignore ├── .npmignore ├── spec ├── support │ └── jasmine.json ├── helpers │ └── dom.js └── ReactTags.spec.js ├── .editorconfig ├── lib ├── Tag.js ├── concerns │ ├── focusNextElement.js │ └── matchers.js ├── Suggestions.js ├── Input.js └── ReactTags.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── test.yml ├── rollup.config.js ├── LICENSE ├── example ├── rollup.config.js ├── main.js ├── countries.js ├── styles.css └── index.html ├── package.json ├── gh-pages.sh ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules 4 | npm-debug.log 5 | dist 6 | example/bundle.js 7 | example/bundle.js.map 8 | coverage 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .github/ 3 | .nyc_output/ 4 | coverage/ 5 | example/ 6 | lib/ 7 | spec/ 8 | .DS_Store 9 | .editorconfig 10 | gh-pages.sh 11 | rollup.config.js 12 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*.spec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_size = 2 9 | indent_style = space 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /lib/Tag.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Tag (props) { 4 | return ( 5 | 8 | ) 9 | } 10 | 11 | export default Tag 12 | -------------------------------------------------------------------------------- /lib/concerns/focusNextElement.js: -------------------------------------------------------------------------------- 1 | export function focusNextElement (scope, currentTarget) { 2 | const interactiveEls = scope.querySelectorAll('a,button,input') 3 | 4 | const currentEl = Array.prototype.findIndex.call( 5 | interactiveEls, 6 | (element) => element === currentTarget 7 | ) 8 | 9 | const nextEl = interactiveEls[currentEl - 1] || interactiveEls[currentEl + 1] 10 | 11 | if (nextEl) { 12 | nextEl.focus() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/concerns/matchers.js: -------------------------------------------------------------------------------- 1 | function escapeForRegExp (string) { 2 | return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') 3 | } 4 | 5 | export function matchAny (string) { 6 | return new RegExp(escapeForRegExp(string), 'gi') 7 | } 8 | 9 | export function matchPartial (string) { 10 | return new RegExp(`(?:^|\\s)${escapeForRegExp(string)}`, 'i') 11 | } 12 | 13 | export function matchExact (string) { 14 | return new RegExp(`^${escapeForRegExp(string)}$`, 'i') 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the solution you'd like 11 | 12 | A clear and concise description of what you want to happen. 13 | 14 | ## Describe alternatives you've considered 15 | 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | ## Additional context 19 | 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /spec/helpers/dom.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Based on: 4 | // 5 | // 6 | 7 | const jsdom = require('jsdom') 8 | 9 | const doc = '
' 10 | 11 | const { window } = new jsdom.JSDOM(doc, { 12 | url: 'http://localhost/' 13 | }) 14 | 15 | global.window = window 16 | 17 | // from mocha-jsdom https://github.com/rstacruz/mocha-jsdom/blob/master/index.js#L80 18 | for (const key in window) { 19 | if (!Object.prototype.hasOwnProperty.call(window, key)) { 20 | continue 21 | } 22 | 23 | if (key in global) { 24 | continue 25 | } 26 | 27 | global[key] = window[key] 28 | } 29 | 30 | // A simple requestAnimationFrame shim which is required by React 31 | window.requestAnimationFrame = (callback) => { 32 | setTimeout(callback, 0) 33 | } 34 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import pkg from './package.json' 3 | 4 | const input = 'lib/ReactTags.js' 5 | 6 | const external = [ 7 | 'react', 8 | 'react-dom', 9 | 'prop-types' 10 | ] 11 | 12 | export default [ 13 | { 14 | input, 15 | external, 16 | plugins: [ 17 | buble({ objectAssign: 'Object.assign', target: { ie: 11 } }) 18 | ], 19 | output: { 20 | name: 'ReactTags', 21 | file: pkg.browser, 22 | format: 'umd' 23 | } 24 | }, 25 | { 26 | input, 27 | external, 28 | plugins: [ 29 | buble({ objectAssign: 'Object.assign', target: { node: 8 } }) 30 | ], 31 | output: { 32 | file: pkg.module, 33 | format: 'es' 34 | } 35 | }, 36 | { 37 | input, 38 | external, 39 | plugins: [ 40 | buble({ objectAssign: 'Object.assign', target: { node: 8 } }) 41 | ], 42 | output: { 43 | file: pkg.main, 44 | format: 'cjs', 45 | exports: 'default' 46 | } 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [14.x, 16.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | 29 | - name: Install 30 | run: npm install 31 | 32 | - name: Lint and format 33 | run: npm run lint 34 | 35 | - name: Build 36 | run: npm run build 37 | 38 | - name: Test 39 | run: npm run coverage 40 | 41 | - name: Prepare coverage report 42 | run: npx nyc report --reporter=lcovonly 43 | 44 | - name: Send coverage report to Coveralls 45 | uses: coverallsapp/github-action@master 46 | with: 47 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: i-like-robots 7 | 8 | --- 9 | 10 | Your issue may already be reported! Please search on the [issue tracker](../) before creating one. 11 | 12 | ## Expected behaviour 13 | 14 | A clear and concise description of what the bug is. 15 | 16 | ## Current behaviour 17 | 18 | A clear and concise description of what you expected to happen. 19 | 20 | ## Steps to Reproduce 21 | 22 | Steps to reproduce the problem: 23 | 24 | 1. 25 | 2. 26 | 3. 27 | 4. 28 | 29 | ## Example and screenshots 30 | 31 | Please add a link to a minimal reproducible example of the bug if you have created one. 32 | 33 | ## Screenshots 34 | 35 | If applicable, add screenshots to help explain your problem. 36 | 37 | ## Your environment 38 | 39 | - OS: [e.g. Windows 10] 40 | - Browser: [e.g. chrome 76, safari 12] 41 | - Version of the component: [e.g. 5.11.2] 42 | - React version: [e.g. 16.12] 43 | 44 | ## Additional context 45 | 46 | Add any other useful context about the problem here. 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Matt Hinchliffe 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 | -------------------------------------------------------------------------------- /example/rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from '@rollup/plugin-buble' 2 | import serve from 'rollup-plugin-serve' 3 | import replace from '@rollup/plugin-replace' 4 | import commonjs from '@rollup/plugin-commonjs' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import { terser } from 'rollup-plugin-terser' 7 | 8 | const plugins = [ 9 | // Use Buble plugin to transpile JSX 10 | buble({ 11 | objectAssign: 'Object.assign', 12 | exclude: 'node_modules/**' 13 | }), 14 | // Use replace plugin to set environment variables 15 | replace({ 16 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 17 | }), 18 | // Use resolve plugin to use Node's module resolution algorithm 19 | resolve(), 20 | // Use CommonJS plugin to include non-ES modules 21 | commonjs() 22 | ] 23 | 24 | if (process.env.NODE_ENV === 'production') { 25 | // Use Terser plugin to minify output 26 | plugins.push(terser()) 27 | } else { 28 | plugins.push(serve({ open: true, contentBase: 'example', port: 8080 })) 29 | } 30 | 31 | export default { 32 | input: 'example/main.js', 33 | plugins, 34 | output: { 35 | file: 'example/bundle.js', 36 | format: 'iife', 37 | sourcemap: true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/Suggestions.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { matchAny } from './concerns/matchers' 3 | 4 | function markIt (name, query) { 5 | const regexp = matchAny(query) 6 | return name.replace(regexp, '$&') 7 | } 8 | 9 | function DefaultSuggestionComponent ({ item, query }) { 10 | return ( 11 | 12 | ) 13 | } 14 | 15 | function Suggestions (props) { 16 | const SuggestionComponent = props.suggestionComponent || DefaultSuggestionComponent 17 | 18 | const options = props.options.map((item, index) => { 19 | const key = `${props.id}-${index}` 20 | const classNames = [] 21 | 22 | if (props.index === index) { 23 | classNames.push(props.classNames.suggestionActive) 24 | } 25 | 26 | if (item.disabled) { 27 | classNames.push(props.classNames.suggestionDisabled) 28 | } 29 | 30 | return ( 31 |
  • e.preventDefault()} 38 | onClick={() => props.addTag(item)} 39 | > 40 | {item.prefix 41 | ? {item.prefix}{' '} 42 | : null} 43 | {item.disableMarkIt 44 | ? item.name 45 | : } 46 |
  • 47 | ) 48 | }) 49 | 50 | return ( 51 |
    52 |
      {options}
    53 |
    54 | ) 55 | } 56 | 57 | export default Suggestions 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tag-autocomplete", 3 | "version": "6.3.0", 4 | "description": "React Tag Autocomplete is a simple tagging component ready to drop in your React projects.", 5 | "main": "dist/ReactTags.cjs.js", 6 | "module": "dist/ReactTags.esm.js", 7 | "browser": "dist/ReactTags.umd.js", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "prepublish": "npm run build", 11 | "pretest": "npm run lint && npm run build", 12 | "lint": "standard lib/*.js spec/*.js", 13 | "test": "jasmine", 14 | "coverage": "nyc --include 'dist/**' npm test", 15 | "dev": "rollup --environment=NODE_ENV:development -c example/rollup.config.js --watch", 16 | "example": "rollup --environment=NODE_ENV:production -c example/rollup.config.js", 17 | "build": "rollup -c rollup.config.js" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "tags", 22 | "tag input", 23 | "react-component", 24 | "autosuggest", 25 | "autocomplete" 26 | ], 27 | "author": "Matt Hinchliffe", 28 | "contributors": [ 29 | "Prakhar Srivastav", 30 | "Simon Hötten", 31 | "Andre-John Mas", 32 | "Mike Kamermans", 33 | "Juliette Prétot", 34 | "Andrew Pillsbury", 35 | "Axel Niklasson", 36 | "Serhiy Yefremenko", 37 | "Paul Shannon", 38 | "Herdis Maria", 39 | "Sibiraj S", 40 | "Cristina Lozano", 41 | "Alexander Nestorov", 42 | "Lars Haßler", 43 | "Joel Posti" 44 | ], 45 | "license": "MIT", 46 | "repository": "https://github.com/i-like-robots/react-tags", 47 | "peerDependencies": { 48 | "prop-types": "^15.5.0", 49 | "react": "^16.5.0 || ^17.0.0", 50 | "react-dom": "^16.5.0 || ^17.0.0" 51 | }, 52 | "devDependencies": { 53 | "@rollup/plugin-buble": "^0.21.0", 54 | "@rollup/plugin-commonjs": "^22.0.0", 55 | "@rollup/plugin-node-resolve": "^13.3.0", 56 | "@rollup/plugin-replace": "^4.0.0", 57 | "jasmine": "^4.2.0", 58 | "jsdom": "^19.0.0", 59 | "match-sorter": "^6.3.0", 60 | "nyc": "^15.1.0", 61 | "prop-types": "^15.7.2", 62 | "react": "^16.13.0", 63 | "react-dom": "^16.13.0", 64 | "rollup": "^2.75.0", 65 | "rollup-plugin-serve": "^1.1.0", 66 | "rollup-plugin-terser": "^7.0.2", 67 | "sinon": "^14.0.0", 68 | "standard": "^17.0.0" 69 | }, 70 | "engines": { 71 | "node": ">= 10.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef, useState } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import ReactTags from '../lib/ReactTags' 4 | import suggestions from './countries' 5 | 6 | /** 7 | * Demo 1 - Country selector 8 | */ 9 | 10 | function CountrySelector () { 11 | const [tags, setTags] = useState([]) 12 | 13 | const reactTags = useRef() 14 | 15 | const onDelete = useCallback((tagIndex) => { 16 | setTags(tags.filter((_, i) => i !== tagIndex)) 17 | }, [tags]) 18 | 19 | const onAddition = useCallback((newTag) => { 20 | setTags([...tags, newTag]) 21 | }, [tags]) 22 | 23 | return ( 24 | <> 25 |

    Select the countries you have visited below:

    26 | 34 |

    Output:

    35 |
    {JSON.stringify(tags, null, 2)}
    36 | 37 | ) 38 | } 39 | 40 | ReactDOM.render(, document.getElementById('demo-1')) 41 | 42 | /** 43 | * Demo 2 - Custom tags 44 | */ 45 | 46 | function CustomTags () { 47 | const [tags, setTags] = useState([]) 48 | 49 | const reactTags = useRef() 50 | 51 | const onDelete = useCallback((tagIndex) => { 52 | setTags(tags.filter((_, i) => i !== tagIndex)) 53 | }, [tags]) 54 | 55 | const onAddition = useCallback((newTag) => { 56 | setTags([...tags, newTag]) 57 | }, [tags]) 58 | 59 | const onValidate = useCallback((newTag) => { 60 | return /^[a-z]{3,12}$/i.test(newTag.name) 61 | }) 62 | 63 | return ( 64 | <> 65 |

    Enter new tags meeting the requirements below:

    66 | 76 |

    77 | Tags must be 3–12 characters in length and only contain the letters A-Z 78 |

    79 |

    Output:

    80 |
    {JSON.stringify(tags, null, 2)}
    81 | 82 | ) 83 | } 84 | 85 | ReactDOM.render(, document.getElementById('demo-2')) 86 | -------------------------------------------------------------------------------- /lib/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const SIZER_STYLES = { 4 | position: 'absolute', 5 | width: 0, 6 | height: 0, 7 | visibility: 'hidden', 8 | overflow: 'scroll', 9 | whiteSpace: 'pre' 10 | } 11 | 12 | const STYLE_PROPS = [ 13 | 'fontSize', 14 | 'fontFamily', 15 | 'fontWeight', 16 | 'fontStyle', 17 | 'letterSpacing', 18 | 'textTransform' 19 | ] 20 | 21 | class Input extends React.Component { 22 | constructor (props) { 23 | super(props) 24 | this.state = { inputWidth: null } 25 | 26 | this.input = React.createRef() 27 | this.sizer = React.createRef() 28 | } 29 | 30 | componentDidMount () { 31 | if (this.props.autoresize) { 32 | this.copyInputStyles() 33 | this.updateInputWidth() 34 | } 35 | } 36 | 37 | componentDidUpdate ({ query, placeholderText }) { 38 | if (query !== this.props.query || placeholderText !== this.props.placeholderText) { 39 | this.updateInputWidth() 40 | } 41 | } 42 | 43 | copyInputStyles () { 44 | const inputStyle = window.getComputedStyle(this.input.current) 45 | 46 | STYLE_PROPS.forEach((prop) => { 47 | this.sizer.current.style[prop] = inputStyle[prop] 48 | }) 49 | } 50 | 51 | updateInputWidth () { 52 | let inputWidth 53 | 54 | if (this.props.autoresize) { 55 | // scrollWidth is designed to be fast not accurate. 56 | // +2 is completely arbitrary but does the job. 57 | inputWidth = Math.ceil(this.sizer.current.scrollWidth) + 2 58 | } 59 | 60 | if (inputWidth !== this.state.inputWidth) { 61 | this.setState({ inputWidth }) 62 | } 63 | } 64 | 65 | render () { 66 | const { id, query, ariaLabelText, placeholderText, expanded, classNames, inputAttributes, inputEventHandlers, index } = this.props 67 | 68 | return ( 69 |
    70 | -1 ? `${id}-${index}` : null} 82 | aria-expanded={expanded} 83 | style={{ width: this.state.inputWidth }} 84 | /> 85 |
    {query || placeholderText}
    86 |
    87 | ) 88 | } 89 | } 90 | 91 | export default Input 92 | -------------------------------------------------------------------------------- /example/countries.js: -------------------------------------------------------------------------------- 1 | const countries = ['Afghanistan', 'Albania', 'Algeria', 'Andorra', 'Angola', 'Anguilla', 'Antigua & Barbuda', 'Argentina', 'Armenia', 'Aruba', 'Australia', 'Austria', 'Azerbaijan', 'Bahamas', 'Bahrain', 'Bangladesh', 'Barbados', 'Belarus', 'Belgium', 'Belize', 'Benin', 'Bermuda', 'Bhutan', 'Bolivia', 'Bosnia & Herzegovina', 'Botswana', 'Brazil', 'British Virgin Islands', 'Brunei', 'Bulgaria', 'Burkina Faso', 'Burundi', 'Cambodia', 'Cameroon', 'Cape Verde', 'Cayman Islands', 'Chad', 'Chile', 'China', 'Colombia', 'Congo', 'Cook Islands', 'Costa Rica', 'Cote D Ivoire', 'Croatia', 'Cruise Ship', 'Cuba', 'Cyprus', 'Czech Republic', 'Denmark', 'Djibouti', 'Dominica', 'Dominican Republic', 'Ecuador', 'Egypt', 'El Salvador', 'Equatorial Guinea', 'Estonia', 'Ethiopia', 'Falkland Islands', 'Faroe Islands', 'Fiji', 'Finland', 'France', 'French Polynesia', 'French West Indies', 'Gabon', 'Gambia', 'Georgia', 'Germany', 'Ghana', 'Gibraltar', 'Greece', 'Greenland', 'Grenada', 'Guam', 'Guatemala', 'Guernsey', 'Guinea', 'Guinea Bissau', 'Guyana', 'Haiti', 'Honduras', 'Hong Kong', 'Hungary', 'Iceland', 'India', 'Indonesia', 'Iran', 'Iraq', 'Ireland', 'Isle of Man', 'Israel', 'Italy', 'Jamaica', 'Japan', 'Jersey', 'Jordan', 'Kazakhstan', 'Kenya', 'Kuwait', 'Kyrgyz Republic', 'Laos', 'Latvia', 'Lebanon', 'Lesotho', 'Liberia', 'Libya', 'Liechtenstein', 'Lithuania', 'Luxembourg', 'Macau', 'Macedonia', 'Madagascar', 'Malawi', 'Malaysia', 'Maldives', 'Mali', 'Malta', 'Mauritania', 'Mauritius', 'Mexico', 'Moldova', 'Monaco', 'Mongolia', 'Montenegro', 'Montserrat', 'Morocco', 'Mozambique', 'Namibia', 'Nepal', 'Netherlands', 'Netherlands Antilles', 'New Caledonia', 'New Zealand', 'Nicaragua', 'Niger', 'Nigeria', 'Norway', 'Oman', 'Pakistan', 'Palestine', 'Panama', 'Papua New Guinea', 'Paraguay', 'Peru', 'Philippines', 'Poland', 'Portugal', 'Puerto Rico', 'Qatar', 'Reunion', 'Romania', 'Russia', 'Rwanda', 'Saint Pierre & Miquelon', 'Samoa', 'San Marino', 'Satellite', 'Saudi Arabia', 'Senegal', 'Serbia', 'Seychelles', 'Sierra Leone', 'Singapore', 'Slovakia', 'Slovenia', 'South Africa', 'South Korea', 'Spain', 'Sri Lanka', 'St Kitts & Nevis', 'St Lucia', 'St Vincent', 'St. Lucia', 'Sudan', 'Suriname', 'Swaziland', 'Sweden', 'Switzerland', 'Syria', 'Taiwan', 'Tajikistan', 'Tanzania', 'Thailand', "Timor L'Este", 'Togo', 'Tonga', 'Trinidad & Tobago', 'Tunisia', 'Turkey', 'Turkmenistan', 'Turks & Caicos', 'Uganda', 'Ukraine', 'United Arab Emirates', 'United Kingdom', 'United States of America', 'Uruguay', 'Uzbekistan', 'Venezuela', 'Vietnam', 'Virgin Islands (US)', 'Yemen', 'Zambia', 'Zimbabwe'] 2 | 3 | module.exports = countries.map((name, id) => ({ id, name })) 4 | -------------------------------------------------------------------------------- /example/styles.css: -------------------------------------------------------------------------------- 1 | /** 2 | *
    3 | *
    4 | * 7 | *
    8 | *