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; | |
} | |
} |