Created
June 7, 2018 06:55
-
-
Save ElManouche/aed1ab2bc908e1a7699bbefefdc4586b to your computer and use it in GitHub Desktop.
React 16.2.0 Materialize Autocomplete Chips
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="container modified"> | |
<h2>React 16.2.0 Materialize Autocomplete Chips</h2> | |
<div id="app" /> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class SearchUser extends React.Component { | |
constructor(props) { | |
super(props); | |
// stores the key elements for the suggested items | |
this.suggestedItemRefs = []; | |
this.listRef; | |
this.state = { | |
selectedItems: [], | |
suggestedItems: [], | |
currentSuggestedItemKey: -1 | |
}; | |
} | |
componentDidMount() { | |
this.inputElement.focus() | |
} | |
componentDidUpdate() { | |
if (this.state.currentSuggestedItemKey >= 0 | |
&& this.suggestedItemRefs[this.state.currentSuggestedItemKey]) { | |
Utils.scrollTo(this.suggestedItemRefs[this.state.currentSuggestedItemKey], this.listRef); | |
} | |
} | |
handleChange(event) { | |
let input = event.target.value; | |
let users; | |
if (`${+input}` === input) { | |
users = UsersAPI.search(input); | |
} else if( input !== '') { | |
users = UsersAPI.search(input); | |
} | |
this.setState({ currentSuggestedItemKey: 0}); | |
if(users){ | |
const selectedIds = _.pluck(this.state.selectedItems, 'id'); | |
const result = users.filter(user => !selectedIds.includes(user.id)); | |
this.setState({ suggestedItems: result }); | |
} else { | |
this.resetFound(); | |
} | |
} | |
resetFound() { | |
this.setState({ suggestedItems: [] }); | |
} | |
removeLast() { | |
this.setState(prevState => { | |
selectedItems: prevState.selectedItems.pop() | |
}); | |
} | |
handleKeyPress(event) { | |
if(event.key === 'Enter'){ | |
this.addSelected(); | |
} else if(event.key === 'ArrowDown') { | |
this.selectNext(); | |
event.preventDefault(); | |
} else if(event.key === 'ArrowUp') { | |
this.selectPrev(); | |
event.preventDefault(); | |
} else if(event.key === 'Backspace') { | |
if (this.inputElement.value === '') { | |
this.removeLast(); | |
} | |
} | |
} | |
addSelected() { | |
var user = this.state.suggestedItems[this.state.currentSuggestedItemKey]; | |
if(user && (this.state.selectedItems.indexOf(user) === -1)) { | |
this.setState(prevState => { | |
selectedItems: prevState.selectedItems.push(user) | |
}); | |
this.inputElement.value = ''; | |
this.resetFound(); | |
} | |
} | |
selectNext() { | |
var newKey = this.state.currentSuggestedItemKey + 1; | |
var user = this.state.suggestedItems[newKey]; | |
if(user) { | |
this.setState({ currentSuggestedItemKey: newKey }); | |
} | |
} | |
selectPrev() { | |
var newKey = this.state.currentSuggestedItemKey - 1; | |
var user = this.state.suggestedItems[newKey]; | |
if(user) { | |
this.setState({ currentSuggestedItemKey: newKey }); | |
} | |
} | |
findKey(item) { | |
const key = _.indexOf(this.state.suggestedItems, _.findWhere(this.state.suggestedItems, item)) | |
return key; | |
} | |
setKey(key) { | |
this.setState({ currentSuggestedItemKey: key }); | |
} | |
handleItemHovered(item) { | |
this.setKey(this.findKey(item)); | |
} | |
handleItemClicked(item) { | |
this.setKey(this.findKey(item)); | |
this.addSelected(); | |
} | |
removeUser(user) { | |
this.setState(prevState => { | |
selectedItems: Utils.remove(prevState.selectedItems, user) | |
}); | |
} | |
renderChips() { | |
return this.state.selectedItems.map((user, k) => { | |
return ( | |
<Chip key={ k } | |
content={ user.name } | |
closeCallBack={ () => this.removeUser(user) } /> | |
); | |
}); | |
} | |
setListRef(ref) { | |
this.listRef = ref; | |
} | |
setItemRef(ref) { | |
if(ref){ | |
this.suggestedItemRefs[ref.getAttribute('data-key')] = ref; | |
} else{ | |
if (!_.isEmpty(this.suggestedItemRefs)) { | |
this.suggestedItemRefs = []; | |
} | |
} | |
} | |
renderSuggestions() { | |
const style = { | |
position: 'absolute', | |
width: '100%', | |
marginTop: 0, | |
overflow: 'auto', | |
maxHeight: '250px', | |
overflowY: 'auto', | |
zIndex: '3' | |
} | |
return !_.isEmpty(this.state.suggestedItems) && ( | |
<List setRef={ this.setListRef.bind(this) } | |
ref={ el => this.listComp = el } | |
items={ this.state.suggestedItems } | |
itemTemplate={ UserItem } | |
itemProps={{ | |
onMouseEnterCallback: this.handleItemHovered.bind(this), | |
onClickCallback: this.handleItemClicked.bind(this), | |
setRef: this.setItemRef.bind(this) | |
}} | |
selectedItemKey={ this.state.currentSuggestedItemKey } | |
style={ style } /> | |
); | |
} | |
render() { | |
const ids = _.pluck(this.state.selectedItems, 'id').sort((a, b) => a - b); | |
return ( | |
<div style={{ marginTop: '48px' }}> | |
<div className="chips input-field" onClick={ () => this.inputElement.focus() }> | |
{ this.renderChips() } | |
<input ref={ el => this.inputElement = el } | |
id="search-user" | |
className="input" | |
type="text" | |
onChange={ this.handleChange.bind(this) } | |
onKeyDown={ this.handleKeyPress.bind(this) } | |
placeholder="Enter ids or Email..." /> | |
{ this.renderSuggestions() } | |
<label className="active" htmlFor="search-user">User Search</label> | |
</div> | |
<div className="input-field" style={{ opacity: '0.5', marginTop: '96px' }}> | |
<input disabled={ true } | |
value={ ids.join(', ') } | |
id="disabled" | |
type="text" | |
className="validate" | |
placeholder="comma separated ids..." /> | |
<label htmlFor="disabled" className="active">Values stored</label> | |
</div> | |
</div> | |
); | |
} | |
} | |
class List extends React.Component { | |
render() { | |
const ItemTemplate = this.props.itemTemplate; | |
let newProps = _.extend({}, this.props); | |
['setRef', 'items', 'itemTemplate', 'itemProps', 'selectedItemKey'].forEach((attr)=>{ | |
delete newProps[attr] | |
}); | |
return ( | |
<ul className="collection" { ...newProps } ref={ this.props.setRef }> | |
{ | |
this.props.items.map((item, k) => { | |
return (<ItemTemplate item={ item } | |
key={ k } | |
dataKey={ k } | |
isActive={ k === this.props.selectedItemKey } | |
{ ...this.props.itemProps } />) | |
}) | |
} | |
</ul> | |
); | |
} | |
} | |
class UserItem extends React.Component { | |
handleClicked() { | |
this.props.onClickCallback(this.props.item); | |
} | |
handleHovered() { | |
this.props.onMouseEnterCallback(this.props.item) | |
} | |
render() { | |
const user = this.props.item; | |
let classname = "collection-item avatar"; | |
if (this.props.isActive) { | |
classname += ' active'; | |
} | |
return ( | |
<li className={ classname } | |
onMouseEnter={ this.handleHovered.bind(this) } | |
onClick={ this.handleClicked.bind(this) } | |
ref={ this.props.setRef } | |
data-key={ this.props.dataKey }> | |
<i className="material-icons circle">person</i> | |
<span className="title">{ user.name }</span> | |
<p>{ user.email } - id: { user.id }</p> | |
{ this.props.isActive && ( | |
<a href="#!" className="secondary-content"> | |
<i className="material-icons">check_circle</i> | |
</a> | |
) } | |
</li> | |
); | |
} | |
} | |
class Chip extends React.Component { | |
render() { | |
return ( | |
<div className="chip"> | |
{this.props.content} | |
{ this.props.closeCallBack && ( | |
<i className="close material-icons" onClick={ this.props.closeCallBack.bind(this) }>close</i> | |
)} | |
</div> | |
); | |
} | |
} | |
/** NON JSX ELEMENTS **/ | |
class Utils { | |
/** | |
* Looks through the list and returns the first value that matches all of the key-value pairs | |
* listed in properties | |
* | |
* @param {object[]} list | |
* @param {object} properties | |
* @returns {object} the list item with the correct properties | |
*/ | |
static findWhere(list, properties) { | |
return list.find(item => Object.keys(properties).every(key => item[key] === properties[key])); | |
} | |
/** | |
* Search for a string in all object values | |
* @param {object[]} list | |
* @param {string} value | |
* @returns {object[]} all items matching the passed value | |
*/ | |
static filterByValue(list, value) { | |
return _.filter(list, function (obj) { | |
return _.values(obj).some(function (el) { | |
return (`${el}`.search(new RegExp(value, "i")) !== -1) | |
}); | |
}); | |
} | |
/** | |
* Removes elements from an array | |
* @param {object[]} | |
* @param [a] arguments - the items to remove | |
* @returns a copy of the array without the passed elements | |
*/ | |
static remove(arr) { | |
var what, a = arguments, L = a.length, ax; | |
while (L > 1 && arr.length) { | |
what = a[--L]; | |
while ((ax= arr.indexOf(what)) !== -1) { | |
arr.splice(ax, 1); | |
} | |
} | |
return arr; | |
} | |
/** | |
* Make visible an element of a scrollable container | |
* @param {object} element - DOM element inside the container | |
* @param {object} container - DOM element of the container | |
*/ | |
static scrollTo(element, container) { | |
if (element.offsetTop < container.scrollTop) { | |
container.scrollTop = element.offsetTop; | |
} else { | |
const offsetBottom = element.offsetTop + element.offsetHeight; | |
const scrollBottom = container.scrollTop + container.offsetHeight; | |
if (offsetBottom > scrollBottom) { | |
container.scrollTop = offsetBottom - container.offsetHeight; | |
} | |
} | |
} | |
}; | |
// To simulate ajax calls | |
UsersAPI = { | |
USERS: [ | |
{ id: 1, name: 'Arthur', email: '[email protected]'}, | |
{ id: 2, name: 'Bertrand', email: '[email protected]'}, | |
{ id: 3, name: 'Christos', email: '[email protected]'}, | |
{ id: 4, name: 'Daniel', email: '[email protected]'}, | |
{ id: 5, name: 'Eléonore', email: '[email protected]'}, | |
{ id: 6, name: 'Fabian', email: '[email protected]'}, | |
{ id: 7, name: 'Gertrude', email: '[email protected]'}, | |
{ id: 8, name: 'Hercules', email: '[email protected]'}, | |
{ id: 9, name: 'Igor', email: '[email protected]'}, | |
{ id: 10, name: 'Joann', email: '[email protected]'}, | |
{ id: 11, name: 'Karim', email: '[email protected]'}, | |
{ id: 12, name: 'Léon', email: '[email protected]'}, | |
{ id: 13, name: 'Manu', email: '[email protected]'}, | |
{ id: 14, name: 'Nathan', email: '[email protected]'}, | |
{ id: 15, name: 'Oscar', email: '[email protected]'}, | |
{ id: 16, name: 'Pascal', email: '[email protected]'}, | |
{ id: 17, name: 'Quentin', email: '[email protected]'}, | |
{ id: 18, name: 'Raoul', email: '[email protected]'}, | |
{ id: 19, name: 'Sam', email: '[email protected]'}, | |
{ id: 20, name: 'Tom', email: '[email protected]'}, | |
{ id: 21, name: 'Ursule', email: '[email protected]'}, | |
{ id: 22, name: 'Véro', email: '[email protected]'}, | |
{ id: 23, name: 'Wolfgang', email: '[email protected]'}, | |
{ id: 24, name: 'Xavier', email: '[email protected]'}, | |
{ id: 25, name: 'Yves', email: '[email protected]'}, | |
{ id: 26, name: 'Zorro', email: '[email protected]'} | |
], | |
getById: function(id) { | |
return Utils.findWhere(UsersAPI.USERS, {id: +id}); | |
}, | |
getByEmail: function(email) { | |
return Utils.findWhere(UsersAPI.USERS, {email: email}); | |
}, | |
search: function(string) { | |
return Utils.filterByValue(UsersAPI.USERS, string); | |
} | |
}; | |
// Display the React element | |
ReactDOM.render( | |
<SearchUser />, | |
document.getElementById('app') | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.development.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.development.js"></script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.modified label { | |
width: calc(100% - 3rem - 1.5rem) !important; | |
&:active { | |
color: #009688; | |
} | |
} | |
.modified label.focus { | |
color: #26a69a; | |
} | |
.modified .prefix ~ .chips { | |
margin-left: 3rem; | |
width: 92%; | |
width: calc(100% - 3rem); | |
} | |
.modified .chips { | |
min-height: 3rem; | |
padding-bottom: 0; | |
} | |
.modified .chips input { | |
font-size: 1rem; | |
line-height: 1rem; | |
height: 3rem; | |
margin-bottom: 0; | |
} | |
.modified .chip { | |
padding: 0 0.75rem; | |
margin-bottom: 0; | |
height: 25px; | |
line-height: 25px; | |
border-radius: 12.5px; | |
margin-top: 9px; | |
font-size: 14px; | |
} | |
.modified .chip .material-icons { | |
line-height: 25px; | |
font-size: 12.5px; | |
} | |
.modified .chips:empty ~ label { | |
font-size: 0.8rem; | |
transform: translateY(-140%); | |
} | |
.chips input[type=text]:not(.browser-default) { | |
border: 0 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.100.2/css/materialize.min.css" rel="stylesheet" /> | |
<link href="https://cdnjs.cloudflare.com/ajax/libs/material-design-icons/3.0.1/iconfont/material-icons.css" rel="stylesheet" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment