Using multiple filters re-renders component - javascript

I use filters in an application which limit the output of a list of users. See the example of the same concept in my codesandbox
The idea is that the list accepts multiple filter values to narrow down the search as much as needed. The first filter works fine when typing in any characters, but switching over and then also typing something inside the second filter input rerenders the list and overwrites the search findings of the first filter.
Main Component:
<div className="App">
<Filters onChange={this.handleFilter} />
<div className="list">
<List users={filteredUsers} />
</div>
</div>
Filter Inputs:
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onChange={props.onChange}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onChange={props.onChange}
/>
</div>
filter handler:
handleFilter = event => {
const target = event.target;
let updateUsers = this.state.users;
updateUsers = updateUsers.filter(user => {
let type;
if (target.name === "name") {
type = user.name;
} else if (target.name === "email") {
type = user.email;
}
return type.toLowerCase().search(target.value.toLowerCase()) !== -1;
});
this.setState({ filteredUsers: updateUsers });
};
I do plan on using several more filters and the list should not re-render.
What is a way to prevent this or work out a better solution?
How to big websites apply their filters?

You could set filter values of email and name both to state, then trigger a filter function that uses both of those values for the final result. You should also store the values of filter in state, either that of component or a parent. This way you shouldn't have your search values overwritten.
import React, { Component } from "react";
const Filters = ({ onChange, emailFilterValue, nameFilterValue }) => (
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onChange={onChange}
value={nameFilterValue}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onChange={onChange}
value={emailFilterValue}
/>
</div>
);
class Main extends Component {
constructor(props) {
super(props);
this.state = {
users: [],
nameFilter: "",
emailFilter: ""
};
}
handleFilter = e => {
const { name, value } = e.target;
if (name === "email") {
this.setState({ emailFilter: value });
} else if (name === "name") {
this.setState({ nameFilter: value });
}
this.filterUsers();
};
filterUsers = () => {
const { users, nameFilter, emailFilter } = this.state;
let updateUsers = users.slice();
if (nameFilter.length > "") {
// do your search based on name
}
if (emailFilter.length > "") {
// then apply email filter
}
this.setState({ users: updateUsers });
};
render() {
return (
<div className="App">
<Filters
onChange={this.handleFilter}
emailFilterValue={this.state.emailFilter}
nameFilterValue={this.state.nameFilter}
/>
<div className="list">
<List users={filteredUsers} />
</div>
</div>
);
}
}

Here is a sample of my solution. Generally "big websites" reduce the amount of time filtering as much as possible if it's unnecessary. I would equally add a setTimeout() to not filter on EVERY input immediately, and clear said timeout if the user types within 400ms (for example).
In your filter component I would keep track of the filtered words:
class Filters extends React.Component {
constructor(props) {
super(props);
this.state = {
email: "",
name: ""
};
}
handleChange = event => {
const { onChange } = this.props;
let key = "email";
if (event.target.name === "name") {
key = "name";
}
this.setState({ [key]: event.target.value });
onChange({ email: this.state.email, name: this.state.name });
};
render() {
const { onChange } = this.props;
return (
<div className="filters">
<input
name="name"
type="text"
placeholder="Search by name"
onInput={this.handleChange}
/>
<input
name="email"
type="text"
placeholder="Search by email"
onInput={this.handleChange}
/>
</div>
);
}
}
And in your App component I would create two different filters, this way it puts a priority on your "name" property if it's filled out:
handleFilter = data => {
let updateUsers = this.state.users;
console.log(data);
if (data.name.length > 0) {
updateUsers = updateUsers.filter(user => {
return user.name.toLowerCase().includes(data.name);
});
}
if (data.email.length > 0) {
updateUsers = updateUsers.filter(user => {
return user.email.toLowerCase().includes(data.email);
});
}
this.setState({ filteredUsers: updateUsers });
};

Related

How to add input validation in react?

I am having a simple form that has firstName and lastName.
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
For this I am trying to add validation.
The validation rules are,
Both fields should accept only text
First name is required and should have at least 4 characters.
If Last name field has value, then it needs to be at least 3 characters.
Things I have tried to achieve this,
components/utils.js
export function isLettersOnly(string) {
return /^[a-zA-Z]+$/.test(string);
}
components/basic_details.js
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
return;
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
On handle input field, I am making the validation to check whether the input has value but I am unable to get the point how to catch the actual validation error and display below respective input box.
Kindly please help me to display the validation message on the respective fields.
Working example:
I suggest adding an errors property to the form data in form_context:
const [formValue, setFormValue] = useState({
basicDetails: {
firstName: '',
lastName: '',
profileSummary: '',
errors: {},
},
...
});
Add the validation to basic_details subform:
const ErrorText = ({ children }) => (
<div style={{ color: 'red' }}>{children}</div>
);
const BasicDetails = () => {
const [value, setValue] = React.useContext(FormContext);
const { basicDetails } = value;
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: 'Can have only letters.',
},
},
}));
return;
}
switch (name) {
case 'firstName': {
const error = value.length < 4 ? 'Length must be at least 4.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
case 'lastName': {
const error = value.length < 3 ? 'Length must be at least 3.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
default:
// ignore
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
return (
<>
<br />
<br />
<div className="form-group col-sm-6">
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.firstName && (
<ErrorText>{basicDetails.errors.firstName}</ErrorText>
)}
<br />
<br />
<div className="form-group col-sm-4">
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.lastName && (
<ErrorText>{basicDetails.errors.lastName}</ErrorText>
)}
<br />
</>
);
};
Lastly, check the field values and errors to set the disabled attribute on the next button in index.js. The first !(value.basicDetails.firstName && value.basicDetails.lastName) condition handles the initial/empty values state while the second condition handles the error values.
{currentPage === 1 && (
<>
<BasicDetails />
<button
disabled={
!(
value.basicDetails.firstName && value.basicDetails.lastName
) ||
Object.values(value.basicDetails.errors).filter(Boolean).length
}
onClick={next}
>
Next
</button>
</>
)}
This pattern can be repeated for the following steps.
First you must be getting converting controlled component to uncontrolled component error in your console. For controlled component it is always preferred to use state to set value for the input. And with onChange handler you set the state. I will try to put into a single component so you would get the idea and apply your case
import React, {useState} from 'react';
import {isLettersOnly} from './components/utils'; // not sure where it is in your folder structure
const MyInputComponent = ({value, ...props}) => {
const [inputValue, setInputValue] = useState(value || ''); // input value should be empty string or undefined. null will not be accepted.
const [error, setError] = useState(null);
const handleChange = event => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setError('Invalid Input');
}
setInputValue(value);
}
return (
<>
<input
value={inputValue}
onChange={handleChange}
{...props}
/>
{error && (
<span className={"error"}>{error}</span>
)}
</>
)
}
export default MyInputComponent;
This is a very rudimentary component. just to show the concept. You can then import this component as your input field and pass necessary props like name, className etc from parent.
import React from 'react';
import MyInputComponent from 'components/MyInputComponent';
const MyForm = (props) => {
return props.data && props.data.map(data=> (
<MyInputComponent
name="lastName"
className="form-control"
value={data.lastName}
));
}

How to automatically generate Input fields related to list of items React.JS

I'm having a problem in dynamically generating input fields in react. So I have a keys state and I need to have an input field for each key and also I have tried this link: How to implement a dynamic form with controlled components in ReactJS?
but the problem is that the my values state is empty so it will render nothing regarding keys and when I did it with this.ModifyList() it shows input fields regarding each key but it does not have onChange mehtod. the onChange method causes the error when using the this.createUI() .
Also in the end I would like to submit the values of input fields.
Is there any suggestion on how to solve this problem?
my Code Below:
export class FileUploadComponent extends Component {
constructor(props) {
super(props);
this.state = {
//Keys: [],
//values: [],
modify: { Keys: ['key1' , 'key2' , 'key3'], values: [] }
}
this.handleSubmit = this.handleSubmit.bind(this);
}
createUI() {
const { modify } = this.state;
const keys = modify.Keys
const values = modify.values
const val = keys.map(function (item, i) {
values.map(function (el, i) {
return <div key={i}>
<label>{item}</label>
<input type="text" onChange={this.handleChange.bind(this, i)} />
</div>
})
});
return val;
}
handleChange(event, i) {
const {modify} = this.state;
let values = [...modify.values];
values[i] = event.target.value;
this.setState({ values: values });
}
handleSubmit(event) {
alert('A name was submitted: ' + this.state.values.join(', '));
event.preventDefault();
}
ModifyList() {
const { modify } = this.state;
const keys = modify.Keys
const val = keys.map(function (item, i) {
return <div>
<label>{item}</label>
<input type="text" />
</div>
});
return val;
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{/*this.ModifyList()*/}
{this.createUI()}
<input type="submit" className="btn btn-primary" value="Search !" />
<input type="submit" className="btn btn-primary" value="Edit !" />
</form>
</div>
)
}
}
export default FileUploadComponent
You have some some scope issue. One of the main difference between a fat-arrow function and a function declared with the function keyword is that the latter has its own scope, meaning that if you call this inside of it, you are referencing its scope.
In your createUI function, switch your functions to fat-arrow functions and you are all set. Just remember to bind your handle change function in your constructor.
export class FileUploadComponent extends Component {
constructor(props) {
super(props);
this.state = {
//Keys: [],
//values: [],
modify: { Keys: ["key1", "key2", "key3"], values: [""] }
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
createUI() {
const { modify } = this.state;
const keys = modify.Keys;
const values = modify.values;
const val = keys.map((item, i) => {
return values.map((el, i) => {
return (
<div key={i}>
<label>{item}</label>
<input
type="text"
onChange={(event) => this.handleChange(event, i)}
/>
</div>
);
});
});
return val;
}
handleChange(event, i) {
const { modify } = this.state;
let values = [...modify.values];
values[i] = event.target.value;
this.setState({ values: values });
}
handleSubmit(event) {
alert("A name was submitted: " + this.state.values.join(", "));
event.preventDefault();
}
ModifyList() {
const { modify } = this.state;
const keys = modify.Keys;
const val = keys.map(function (item, i) {
return (
<div>
<label>{item}</label>
<input type="text" />
</div>
);
});
return val;
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
{/*this.ModifyList()*/}
{this.createUI()}
<input type="submit" className="btn btn-primary" value="Search !" />
<input type="submit" className="btn btn-primary" value="Edit !" />
</form>
</div>
);
}
}

clear search box after onclick

I have a search box in my header. i can clear my state after searching but the input doesn't get cleared.
I'm purely using the Searchbox to generate a dropdown that contains links to their respective field. So the input field is purely used to mimic a searc
I tried targeting it with refs but when i finally reach the value i can't use the search anymore.
There is a ref for SearchBarHeader, SearchBox and SearchField. But i'm not sure if that is the correct way to do it.
clearSearchBar = () => {
this.searchBarHeader.current.searchBox.current.searchField.current.value = '';
};
and the code for the searchbox.
class Search extends Component {
state = {
organisationNames: [],
errorMessage: null,
};
searchField = React.createRef();
async componentDidMount() {
const organisations = await getSearch();
this.setState({
organisationNames: organisations,
});
}
searchHandler = (e) => {
const searchValue = e.target.value.toLowerCase();
if (!searchValue) {
this.props.clearSearchResult();
} else {
const result = this.state.organisationNames.filter((organisationName) => {
return organisationName.toLowerCase().includes(searchValue);
});
this.props.setSearchResult(result, () => {
if (this.props.searchResult.length === 0) {
this.setState({
errorMessage: "No Results...",
});
} else {
this.setState({
errorMessage: null,
});
}
});
}
};
clearSearchInput = () => {
this.props.clearSearchResult();
};
render() {
return (
<div className="search">
<div className="form-group">
<input
ref={this.searchField}
type="search"
placeholder="Search for company"
onChange={this.searchHandler}
/>
</div>
<div className="search-result-wrapper">
<ul className="search-results">
{this.props.searchResult === undefined ? (
<Skeleton />
) : (
this.props.searchResult.map((res, id) => {
return (
<Link
key={id}
to={"/r/" + res}
onClick={this.clearSearchInput}
>
<li className="search-item">{res || <Skeleton />} </li>
</Link>
);
})
)}
{this.state.errorMessage === null ? (
""
) : (
<li>{this.state.errorMessage}</li>
)}
</ul>
</div>
</div>
);
}
}
export default Search;
It seems to me that you're missing the "value" attribute on your input that makes it reactive to changes in your state. Grabbing one example from react docs, here's the ideal setup:
this.state = {value: ''};
(...)
handleChange(event) {
this.setState({value: event.target.value});
}
(...)
<input type="text" value={this.state.value} onChange={this.handleChange} />
By following the method above, you won't need to use refs to manually clear the input value. Once the form is submitted, you can simply clear your state...
this.setState({value: ''});
... and your input should be cleared.
Here's the link for the docs: https://reactjs.org/docs/forms.html
You are clearing the ref, not the state. There is also not a value attached to your input, so even if the state was cleared, it will not reflect.
You will of course be able to make the form data more dynamic, without having to set and keep companyName constant.
Here is a simple working example is here: https://codesandbox.io/s/flamboyant-voice-oj85u?file=/src/App.js
export default function App() {
const [formData, setFormData] = useState({ companyName: "" });
const handleChange = (e) => {
setFormData({ companyName: e.target.value });
};
const handleClear = () => {
setFormData({ companyName: "" });
};
return (
<div className="search">
<div className="form-group">
<input
type="search"
name="companyName"
value={formData.companyName}
placeholder="Search for company"
onChange={handleChange}
/>
<button onClick={handleClear}>Clear</button>
</div>
<pre>{JSON.stringify(formData, null, 2)}</pre>
</div>
);
}

React refactor simple input component

I have multiples components who needs input value
so I copy/paste my following code on the render method :
<input type="password"
name="password" value={user.password}
placeholder="password"
onChange={e => this.handleChange(e)}/>
And I copy/paste the handleChange method in all my component :
handleChange(event) {
const {name, value} = event.target
this.setState({
[name]: value,
})
}
I would like to have input in a component and call it from all my components and then get input value to add it to my current state component
Do you have any solution ?
Thanks
If I understand correctly you wan't something like this:
class MyWrapperComponent extends React.Component {
constructor(props) {
this.state = { password: '' }
}
handleChange(event) {
const {name, value} = event.target
this.setState({
[name]: value,
})
}
render() {
return (
<div>
<MyInputComponent value={this.state.password} onChange={this.handeChange.bind(this)} />
<MyDisplayPasswordComponent password={this.state.password} />
</div>
)
}
}
const MyInputComponent = (props) => {
return (
<input type="password"
name="password" value={props.password}
placeholder="password"
onChange={props.onChange}/>
)
}
const MyDisplayPasswordComponent = (props) => {
return <h1>Your secret password is {props.password}</h1>
}

React: Add/Change text input base on a selected option

I am trying to display a new text input based on the selected option. I am able to do that as below but the old value entered is always present no matter what I change the new select option to.
What might be a better way to achieve this? Appreciate any suggestions.
class loadComponent extends React.Component {
static propTypes = {
......
};
static defaultProps = {
....
};
constructor() {
super();
this.state = {
value: ""
};
}
state = {
...
};
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
value={this.state.value}
/>
);
};
render() {
let newInputText = '';
if (this.state.selectedInputId !== '') {
newInputText = this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
{newInputText}
);
makeTextInput function creates a new object, but from react's perspective it's the same component because react distinguishes them by looking at their type and key. To make react recreate an element, you have to change one of those values.
This code changes type of NewInputText element each time it renders (because NewInputText always refers to a new function):
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
};
render() {
let NewInputText = () => '';
if (this.state.selectedInputId !== '') {
NewInputText = () => this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
<NewInputText />
);
This code assigns different key to TextInput each time:
reset = (selected) => {
this.setState({
selectedInputName: selected.target[selected.target.selectedIndex].text,
selectedInputId: selected.target.value
});
};
makeTextInput = () => {
return (
<TextInput
key={Math.random()}
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
};
render() {
let newInputText = '';
if (this.state.selectedInputId !== '') {
newInputText = this.makeTextInput();
}
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={selected => this.reset(selected)}
/>
<div className="search margin_bottom_large">
{newInputText}
);
Is there a better way to do this?
I think using the controlled component design pattern would be ideal in this situation.
class SomeInput extends Component {
constructor() {
super();
this.state = {
value: "" //Keep value state here
};
}
render() {
/* Doing something like the following will allow you to clear
the input value simply by doing the following..
this.setState({ value: '' });
*/
return (
<Input
type="text"
onChange={e => this.setState({ value: e.target.value })} // set value state to entered text
value={this.state.value} // set value of input to value piece of state
/>
);
}
}
This will give you full access to the current value of the input, thereby allowing you to set it to anything or clear it at anytime or for any event simply by doing the following this.setState({ value: '' }).
Don't know the rest of your code which could be handy but you can try:
makeTextInput = () => (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
change = (event) => {
this.setState({
selectedInputName: event.target.value
});
}
render() {
return (
<Select
label="What would you like to search with?"
options={this.props.searchOptions}
onChange={this.change}
/>
<div className="search margin_bottom_large">
{this.makeTextInput()}
);
What you need to do is only setState properly. Each time you change a state the component will be re-rendered which means that the makeTextInput method will be triggered.
EDIT:
by the way, it is good idea to use getter for returning component in render method, in this case:
get textInput() {
return (
<TextInput
label={this.state.selectedInputName}
placeholder={`Please enter ${this.state.selectedInputName} here!`}
onBlur={event => this.setState({[this.state.selectedInputId]: event.target.value})}
showClear
/>
);
}
and then in render method, just use {this.textInput}

Categories

Resources