Inspired by Angular Material's md-chips directive.
A Pen by Bronek Szulc on CodePen.
| %h2#title React Chips! | |
| #react-mount |
Inspired by Angular Material's md-chips directive.
A Pen by Bronek Szulc on CodePen.
| const update = React.addons.update; | |
| const Chips = React.createClass({ | |
| propTypes: { | |
| chips: React.PropTypes.array, | |
| max: React.PropTypes.oneOfType([ | |
| React.PropTypes.number, | |
| React.PropTypes.string | |
| ]), | |
| maxlength: React.PropTypes.oneOfType([ | |
| React.PropTypes.number, | |
| React.PropTypes.string | |
| ]), | |
| placeholder: React.PropTypes.string | |
| }, | |
| getDefaultProps() { | |
| return { | |
| placeholder: 'Add a chip...', | |
| maxlength: 20 | |
| }; | |
| }, | |
| getInitialState() { | |
| return { | |
| chips: [], | |
| KEY: { | |
| backspace: 8, | |
| tab: 9, | |
| enter: 13 | |
| }, | |
| // only allow letters, numbers and spaces inbetween words | |
| INVALID_CHARS: /[^a-zA-Z0-9 ]/g | |
| }; | |
| }, | |
| componentDidMount() { | |
| this.setChips(this.props.chips); | |
| }, | |
| componentWillReceiveProps(nextProps) { | |
| this.setChips(nextProps.chips); | |
| }, | |
| setChips(chips) { | |
| if (chips && chips.length) this.setState({ chips }); | |
| }, | |
| onKeyDown(event) { | |
| let keyPressed = event.which; | |
| if (keyPressed === this.state.KEY.enter || | |
| (keyPressed === this.state.KEY.tab && event.target.value)) { | |
| event.preventDefault(); | |
| this.updateChips(event); | |
| } else if (keyPressed === this.state.KEY.backspace) { | |
| let chips = this.state.chips; | |
| if (!event.target.value && chips.length) { | |
| this.deleteChip(chips[chips.length - 1]); | |
| } | |
| } | |
| }, | |
| clearInvalidChars(event) { | |
| let value = event.target.value; | |
| if (this.state.INVALID_CHARS.test(value)) { | |
| event.target.value = value.replace(this.state.INVALID_CHARS, ''); | |
| } else if (value.length > this.props.maxlength) { | |
| event.target.value = value.substr(0, this.props.maxlength); | |
| } | |
| }, | |
| updateChips(event) { | |
| if (!this.props.max || | |
| this.state.chips.length < this.props.max) { | |
| let value = event.target.value; | |
| if (!value) return; | |
| let chip = value.trim().toLowerCase(); | |
| if (chip && this.state.chips.indexOf(chip) < 0) { | |
| this.setState({ | |
| chips: update( | |
| this.state.chips, | |
| { | |
| $push: [chip] | |
| } | |
| ) | |
| }); | |
| } | |
| } | |
| event.target.value = ''; | |
| }, | |
| deleteChip(chip) { | |
| let index = this.state.chips.indexOf(chip); | |
| if (index >= 0) { | |
| this.setState({ | |
| chips: update( | |
| this.state.chips, | |
| { | |
| $splice: [[index, 1]] | |
| } | |
| ) | |
| }); | |
| } | |
| }, | |
| focusInput(event) { | |
| let children = event.target.children; | |
| if (children.length) children[children.length - 1].focus(); | |
| }, | |
| render() { | |
| let chips = this.state.chips.map((chip, index) => { | |
| return ( | |
| <span className="chip" key={index}> | |
| <span className="chip-value">{chip}</span> | |
| <button type="button" className="chip-delete-button" onClick={this.deleteChip.bind(null, chip)}>x</button> | |
| </span> | |
| ); | |
| }); | |
| let placeholder = !this.props.max || chips.length < this.props.max ? this.props.placeholder : ''; | |
| return ( | |
| <div className="chips" onClick={this.focusInput}> | |
| {chips} | |
| <input type="text" className="chips-input" placeholder={placeholder} onKeyDown={this.onKeyDown} onKeyUp={this.clearInvalidChars} /> | |
| </div> | |
| ); | |
| } | |
| }); | |
| ReactDOM.render( | |
| <Chips chips={['react', 'javascript', 'scss']} placeholder="Add a tag..." max="10" />, | |
| document.getElementById('react-mount') | |
| ); |
| <script src="//cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-with-addons.min.js"></script> |
| $chip-y-spacing: 15px; | |
| $chip-x-spacing: 5px; | |
| $chip-button-width: $chip-y-spacing + $chip-x-spacing * 2; | |
| $chip-border-radius: 15px; | |
| $chip-background: #555; | |
| $chip-color: #fff; | |
| $chip-min-height: 36px; | |
| body { | |
| font-family: Arial, sans-serif; | |
| } | |
| #title { | |
| letter-spacing: 1px; | |
| } | |
| .chips { | |
| min-height: $chip-min-height; | |
| border-bottom: 2px solid blue; | |
| line-height: 1; | |
| font-size: 1em; | |
| } | |
| .chips-input { | |
| display: inline-block; | |
| width: 33%; | |
| min-height: $chip-min-height; | |
| margin-bottom: $chip-x-spacing; | |
| margin-left: $chip-x-spacing * 2; | |
| border: 0; | |
| outline: none; | |
| font-size: 0.9rem; | |
| } | |
| .chip { | |
| display: inline-block; | |
| margin-top: $chip-x-spacing; | |
| margin-bottom: $chip-x-spacing; | |
| margin-left: $chip-x-spacing; | |
| margin-right: $chip-button-width; | |
| position: relative; | |
| .chip-value { | |
| display: inline-block; | |
| padding: $chip-x-spacing; | |
| padding-left: $chip-y-spacing; | |
| padding-right: $chip-y-spacing / 2; | |
| background: $chip-background; | |
| color: $chip-color; | |
| font-weight: bold; | |
| border-radius: $chip-border-radius 0 0 $chip-border-radius; | |
| } | |
| .chip-delete-button { | |
| background: $chip-background; | |
| color: $chip-color; | |
| border: 0; | |
| border-radius: 0 $chip-border-radius $chip-border-radius 0; | |
| padding: $chip-x-spacing $chip-x-spacing * 2; | |
| cursor: pointer; | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| right: -$chip-button-width; | |
| line-height: 0.5; | |
| font-weight: bold; | |
| } | |
| } |