Skip to content

Instantly share code, notes, and snippets.

@ElManouche
Created June 7, 2018 06:55
Show Gist options
  • Save ElManouche/aed1ab2bc908e1a7699bbefefdc4586b to your computer and use it in GitHub Desktop.
Save ElManouche/aed1ab2bc908e1a7699bbefefdc4586b to your computer and use it in GitHub Desktop.
React 16.2.0 Materialize Autocomplete Chips
<div class="container modified">
<h2>React 16.2.0 Materialize Autocomplete Chips</h2>
<div id="app" />
</div>
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: 'a@a.com'},
{ id: 2, name: 'Bertrand', email: 'b@a.com'},
{ id: 3, name: 'Christos', email: 'c@a.com'},
{ id: 4, name: 'Daniel', email: 'd@a.com'},
{ id: 5, name: 'Eléonore', email: 'e@a.com'},
{ id: 6, name: 'Fabian', email: 'f@a.com'},
{ id: 7, name: 'Gertrude', email: 'g@a.com'},
{ id: 8, name: 'Hercules', email: 'h@a.com'},
{ id: 9, name: 'Igor', email: 'i@a.com'},
{ id: 10, name: 'Joann', email: 'j@a.com'},
{ id: 11, name: 'Karim', email: 'k@a.com'},
{ id: 12, name: 'Léon', email: 'l@a.com'},
{ id: 13, name: 'Manu', email: 'm@a.com'},
{ id: 14, name: 'Nathan', email: 'n@a.com'},
{ id: 15, name: 'Oscar', email: 'o@a.com'},
{ id: 16, name: 'Pascal', email: 'p@a.com'},
{ id: 17, name: 'Quentin', email: 'q@a.com'},
{ id: 18, name: 'Raoul', email: 'r@a.com'},
{ id: 19, name: 'Sam', email: 's@a.com'},
{ id: 20, name: 'Tom', email: 't@a.com'},
{ id: 21, name: 'Ursule', email: 'u@a.com'},
{ id: 22, name: 'Véro', email: 'v@a.com'},
{ id: 23, name: 'Wolfgang', email: 'w@a.com'},
{ id: 24, name: 'Xavier', email: 'x@a.com'},
{ id: 25, name: 'Yves', email: 'y@a.com'},
{ id: 26, name: 'Zorro', email: 'z@a.com'}
],
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')
);
<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>
.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
}
<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