As part of a technical test, I've been asked to write an autocomplete input in React. I've done this but I'd now like to add the functionality of navigating up and down the rendered list with the arrow keys. I've done some extensive Googling and found nothing React specific apart from npm packages.
To be clear, I'm looking for something like this but for React: https://www.w3schools.com/howto/howto_js_autocomplete.asp
All I basically need is the arrow button functionality, I've got everything else working fine.
Cheers
Here's an example that I tried but couldn't get working.
export default class Example extends Component {
constructor(props) {
super(props)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.state = {
cursor: 0,
result: []
}
}
handleKeyDown(e) {
const { cursor, result } = this.state
// arrow up/down button should select next/previous list element
if (e.keyCode === 38 && cursor > 0) {
this.setState( prevState => ({
cursor: prevState.cursor - 1
}))
} else if (e.keyCode === 40 && cursor < result.length - 1) {
this.setState( prevState => ({
cursor: prevState.cursor + 1
}))
}
}
render() {
const { cursor } = this.state
return (
<Container>
<Input onKeyDown={ this.handleKeyDown }/>
<List>
{
result.map((item, i) => (
<List.Item
key={ item._id }
className={cursor === i ? 'active' : null}
>
<span>{ item.title }</span>
</List.Item>
))
}
</List>
</Container>
)
}
}
And here is my code:
class Search extends Component {
constructor(props) {
super(props);
this.state = {
location: '',
searchName: '',
showSearch: false,
cursor: 0
};
}
handleKeyPress = e => {
const { cursor, searchName } = this.state;
// arrow up/down button should select next/previous list element
if (e.keyCode === 38 && cursor > 0) {
this.setState(prevState => ({
cursor: prevState.cursor - 1
}));
} else if (e.keyCode === 40 && cursor < searchName.length - 1) {
this.setState(prevState => ({
cursor: prevState.cursor + 1
}));
}
};
render() {
const { searchName, location } = this.state;
return (
<div className="Search">
<h1>Where are you going?</h1>
<form id="search-form" onSubmit={this.handleSubmit}>
<label htmlFor="location">Pick-up Location</label>
<input
type="text"
id="location"
value={location}
placeholder="city, airport, station, region, district..."
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
onKeyDown={this.handleKeyPress}
/>
{this.state.showSearch ? (
<Suggestions searchName={searchName} />
) : null}
<button value="submit" type="submit" id="search-button">
Search
</button>
</form>
</div>
);
}
Code that renders the list from the restful API:
.then(res =>
this.setState({
searchName: res.data.results.docs.map(array => (
<a href="#">
<div
key={array.ufi}
className="locations"
>
{array.name}
</div>
</a>
))
})
);
Since you are defining a function as handleKeyDown(e) {...} the context this is not pointing to the context of class instance, the context will be supplied by onKeyDown (and I suppose it's window as this)
So, you have 2 ways to go:
declare your function as handleKeyDown = (e) => {...}
bind handleKeyDown context to component instance like onKeyDown={this.handleKeyDown.bind(this)}
Also, don't forget that you may want a mod items.length counter, meaning when your press down and the last item is already selected, it would go to the first item.
Your api usage with storing markdown into the state is just a terrible thing to do. You don't have access to these strings anymore, instead save it as a plain array. Pass it where you need it, and use it to create jsx there.
Also, you don't use cursor from your state at all.
Related
I am trying to create a component where I have a bunch of boxes from an array, that can be turned 'on' and 'off' when each one is individually clicked.
Currently, only a single item from the array can be switched 'on' (shown by the item turning green), however, I would like to be able to turn each item on/off individually.
Interacting with one element should not affect any of the others.
How do I achieve this?
My click event:
handleOnClick = (val, i) => {
this.setState({active: i}, () => console.log(this.state.active, 'active'))
}
Rendering the boxes:
renderBoxes = () => {
const options = this.state.needsOptions.map((val, i) => {
return (
<button
key={i}
style={{...style.box, background: i === this.state.active ? 'green' : ''}}
onClick={() => this.handleOnClick(val, i)}
>
{val}
</button>
)
})
return options
}
Here's a Codepen
What I would do is to create a Box component with its own active state, and pass this to the map in renderBoxes. The benefit of doing it this way is that each Box component will have its own state independent of the parent. That way you can have more than one component as active.
so...
class Box extends React.Component {
constructor(props){
super(props)
this.state={
active: false
}
}
clickHandler = () => {
this.setState({active: !this.state.active})
}
render(){
const { key, children }= this.props
return (
<button
key={key}
style={{...style.box, background: this.state.active ? 'green' : ''}}
onClick={() => this.clickHandler()}
>
{children}
</button>
)
}
}
then have renderBoxes be...
renderBoxes = () => {
const options = this.state.needsOptions.map((val, i) => {
return (
<Box
key={i}
>
{val}
</Box>
)
})
return options
}
here is the codepen I forked off yours.
I am trying to implement a textbox similar to google flights.
so I have built a react autocomplete prototype.
but in that i am facing an issue.
right now in the google flights textbox when I click on the textbox it shows all the rersults without typing anything.
but in my case if I type something only it will show the results.
so the textbox I added a props onPress in that I am calling an event handleEvent.
but nothing printing inside the method.
can you tell me how to achieve so that in future I will fix it myself.
providing my code snippet and sandbox below
https://codesandbox.io/s/xp6x167kq4
handleEvent = () => {
console.log("I was clicked");
alert("I was clicked");
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul class="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className;
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion} onClick={onClick}>
{suggestion}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div class="no-suggestions">
<em>No suggestions, you're on your own!</em>
</div>
);
}
}
return (
<Fragment>
<input
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
value={userInput}
onPress={this.handleEvent}
//onPress={this.handleEvent}
/>
{suggestionsListComponent}
</Fragment>
);
}
I believe this is because in your initial state you've written
this.state = {
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: '',
};
and later you run
if (showSuggestions && userInput) but on an initial click userInput still equals '' which equates to false. Underneath again, you run if (filteredSuggestions.length) which also equates 0 because when nothing is typed the array filteredSuggestions is empty.
console.log('' == true) => false also
console.log([].length == true) => false
I render a list of items that are contentEditable. When I switch focus to the second element, the first element is still white. I thought the color switch statement (using this.state.active check) would work but clearly, I'm lacking in my thinking here. How would you go about it? Must I implement the logic in Comments component/container instead?
In parent container comments.tsx, I render the comments with;
<div className="comments">
<div>
{comments.map((value, key) => {
return (
<Comment key={key} profile={this.state.profile} data={value}/>
);
})
}
</div>
</div>
</div>
and in comment.tsx, I have;
interface IProps {
key: number;
profile: IProfile;
data: object;
}
export class Comment extends React.Component<IProps, any> {
constructor(props: IProps) {
super(props);
this.state = {
editableColor: 'green',
active:false
}
}
editReview = (e, data) => {
let { _id, user, comm } = data;
this.setState({active: true}, function () {
this.state.active ? this.setState({editableColor:'#ffffff'}) : this.setState({editableColor:'green'});
});
}
render() {
let { key, profile, data } = this.props;
return(
<div className="col-8 comment">
<p id="comment" contentEditable style={{backgroundColor: this.state.editableColor}}
onFocus={(e) => this.editReview(e, data)}
>
{data['comm']}
</p>
<button onClick={(e) => this.update(e, data)}>Update</button>
</div>
);
}
}
It seems to me that you never come back from the active state, you should implement an onBlur event handler to revert state.active to false:
...
<p
id="comment"
contentEditable style={{backgroundColor: this.state.editableColor}}
onFocus={(e) => this.editReview(e, data)}
onBlur={ () => this.setState({ active: false }) }
>
{data['comm']}
</p>
...
Was actually easy. As always I should have passed editableColor as a prop to Component in comments.tsx
<Comment key={key} profile={this.state.profile} data={value} editableColor={'green'}/>
Pull it out as a prop in comment.tsx
let { key, profile, data, editableColor } = this.props;
Switch out the color depending on focus/blur
editReview = (e, data) => {
this.setState({active: true});
}
<p
id="comment"
contentEditable
style={{backgroundColor: this.state.active ? editableColor='#ffffff' : editableColor}}
onFocus={(e) => this.editReview(e, data)}
onBlur={ () => this.setState({ active: false }) }
>
A single click on a button updates all the buttons but I want to change the state of that particular clicked button. Please check the image links below and the code.
import React from 'react';
import './MenuCard.css';
class MenuCard extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
hideButton: true,
aValue: 1,
breads: [],
category: [],
ids: 1,
btnVal: 'Add'
};
}
onKeyCheck = (e) => {
this.state.breads.map(filt => {
if (filt.id === e.target.id) {
console.log(e.target.id + ' and ' + filt.id)
return (this.setState({showButton: !this.state.showButton, hideButton: !this.state.hideButton}));
}
})
}
onShowButton = () => {
this.setState({showButton: !this.state.showButton, hideButton: !this.state.hideButton})
}
onValueIncrease = () => {
this.setState({aValue: this.state.aValue + 1});
}
onValueDecrease = () => {
this.setState({aValue: this.state.aValue - 1});
}
componentDidMount() {
fetch('http://localhost:3000/menu/food_category', {
method: 'get',
headers: {'content-type': 'application/json'}
})
.then(response => response.json())
.then(menudata => {
this.setState({category: menudata.menu_type})
console.log(this.state.category)
})
fetch('http://localhost:3000/menu', {
method: 'get',
headers: {'content-type': 'application/json'}
})
.then(response => response.json())
.then(menudata => {
this.setState({breads: menudata })
})
}
render() {
return (
<div>
{this.state.category.map(types => {
return (<div>
<div className="menu-head">{types}</div>
< div className="container-menu">
{this.state.breads.map((d, id)=> {
if (d.category === types) {
return (
<div>
<div className="content" key={id} id={d.id}>
<div className="items"> {d.item_name}</div>
<div className="prices"> {d.price} Rs.</div>
{this.state.showButton ?
<div>
<button
className="grp-btn-minus"
onClick={this.state.aValue <= 1 ?
() => this.onShowButton() :
() => this.onValueDecrease()}>-
</button>
<input className="grp-btn-text" type="text"
value={this.state.aValue} readOnly/>
<button id={d.id}
className="grp-btn-plus"
onClick={() => this.onValueIncrease()}>+
</button>
</div> :
<button id={d.id} key={id}
onClick={ this.onKeyCheck}
className="add-menu-btn">
add
</button>
}
</div>
</div>
)
}
})}
</div>
</div>)
})}
</div>
)
}
}
export default MenuCard;
This is the first image of multiple rendering of component Add buttons
Here is the problem that all buttons get updated on single click
You're using an array of items but refering to a single, shared value in handlers. De facto you're using a few shared values: showButton, hideButton, aValue), 2/3 unnecessary ;)
First - aValue for each item should be stored in a structure - array or object. It could be an order = {} - object with id-keyed properties with amounts as values like this:
order = {
'masala_id': 1,
'kebab_id' : 2
}
Event handler (for 'add') should check if id for choosen product already exist in order object (as property name) and update amount (+/-) or create new one with 1 value (and remove property when decreased amount = 0).
In practice order should also contain a price - it seams like duplicating data but it will be much easier to count total order value.
order = {
'masala_id': {
'amount': 1,
'price': 20,
},
'kebab_id' : {
'amount': 2,
'price': 180,
}
}
Item doesn't need to be a component but it's much easier to maintain it, keep it readable etc.
This way we can simply pass already ordered amount and conditionally render buttons:
<Product id={d.id}
name={d.item_name}
price={d.price}
amount={order[d.id] ? order[d.id].amount : 0 }
amountHandler={this.changeAmountHandler}
/>
Product should be slightly improved and simplified (f.e. key is needed on top div):
class Product extends React.Component {
render () {
const (id, name, price, amount, amountHandler} = this.props;
const showIncrease = !!amount; // boolean, it also means "don't show add button"
return (
<div key={id} >
<div className="content">
<div className="items">{name}</div>
<div className="prices">{price} Rs.</div>
{showIncrease ?
<div>
<button
className="grp-btn-minus"
onClick={(e) => { amountHandler(e, id, -1) }}
>-</button>
<input className="grp-btn-text" type="text"
value={amount}
readOnly/>
<button
className="grp-btn-plus"
onClick={(e) => { amountHandler(e, id, 1) }}
>+</button>
</div> :
<button
onClick={(e) => { amountHandler(e, id, 1) }}
className="add-menu-btn"
>add</button>
}
</div>
</div>
)}}
This way you can handle all events in one handler, keep entire order state in main component... in case of performance problems just use PureComponent.
It looks like all the buttons are sharing the same state. You could try breaking the button up into its own component, and then move the state that button needs into there. That way when you click a button the state of that one particular button is updated, and not the state of the parent component that contains all the buttons.
I have a component of basic editable list items that operates as such:
My problem is that the cursor does not move into the input box onChange, making a hassle for the user to always click twice. I tried https://coderwall.com/p/0iz_zq/how-to-put-focus-at-the-end-of-an-input-with-react-js but it did not work. Component looks like:
import React from 'react';
import MyComponent from '../utils/MyComponent';
export default class BasicList extends MyComponent {
constructor(props) {
let custom_methods = ['renderItemOrEditField', 'toggleEditing', 'moveCaretAtEnd'];
super(props, custom_methods);
this.state = {editing: null};
}
moveCaretAtEnd(e) {
var temp_value = e.target.value
e.target.value = ''
e.target.value = temp_value
}
renderItemOrEditField(item) {
console.log(item);
if (this.state.editing === item.id) {
return (
<input
onKeyDown={ this.handleEditField }
type="text"
className="form-control"
ref={ `${item.type}_name_${ item.id }` }
name="title"
autofocus
onFocus={this.moveCaretAtEnd}
defaultValue={ item.name }
/>
);
} else {
return (
<li
onClick={this.toggleEditing.bind(null, item.id)}
key={item.id}
className="list-group-item">
{item.name}
</li>
);
}
}
toggleEditing(item_id) {
this.setState({editing: item_id});
}
render() {
let li_elements = null;
let items = this.props.items;
if (items.length > 0) {
li_elements = items.map((item) => {
return (
this.renderItemOrEditField(item)
// {/* }<li key={item.id}>
// {item.name} -
// <button onClick={() => {this.props.deleteCallback(this.props.item_type, item.id, item.name)} }>
// Delete
// </button>
// </li> */}
);
});
}
return (
<div>
<h4>{this.props.title}:</h4>
<ul className="list-group">
{li_elements}
</ul>
</div>
);
}
}
The items I'm working with now only have a name and ID (type denotes a 'role' or 'task')
How can I make the cursor start at the end of the input box text on change?
Autofocus triggers when a component is mounted. Not sure that's happening by your conditional render. You could moving your conditional logic to a separate container and that should trigger a mount each time it's shown.
I ended up setting focus as in the callback that causes the input box to show:
toggleEditing(item_id) {
// this syntax runs the function after this.setState is finished
this.setState({editing: item_id}, function() {
this.textInput.focus();
});
}
Then the original solution given worked:
// https://coderwall.com/p/0iz_zq/how-to-put-focus-at-the-end-of-an-input-with-react-js
moveCaretAtEnd(e) {
var temp_value = e.target.value
e.target.value = ''
e.target.value = temp_value
}
and
<input
onKeyDown={ this.handleEditField }
type="text"
className="form-control"
ref={(input) => { this.textInput = input; }}
name="title"
autofocus
onFocus={this.moveCaretAtEnd}
defaultValue={ item.name }
onChange={(event) => this.editItem(event)}
style={ {maxWidth: 500} }
/>