Instant Search on Material UI AutoComplete Component - javascript

Forced to press enter twice on form submission with AutoComplete Material UI component. I want to be able to only select the options once to navigate to another web page.
Submission Handler
Component Code
I attempted to change around the submission handler and the paper/box components, but the problem persists. Once I select an option from the dropdown menu from AutoComplete the user has to hit enter again.

You implement instant search on the AutoComplete component, using useEffect ReactJS.
import React, { useState, useEffect } from 'react';
import { Autocomplete } from '#material-ui/lab';
import { TextField } from '#material-ui/core';
function searchAutocomplete({ option, onSelection }) {
const [searchTermValue, setSearchTermValue] = useState('');
const [filteredOptionValue, setFilteredOptionValues] = useState([]);
useEffect(() => {
setFilteredOptionValues(option.filter((option) =>
option.label.toLowerCase().includes(searchTermValue.toLowerCase())
));
}, [searchTermValue, option]);
return (
<Autocomplete
option={filteredOptionValue}
getOptionLabel={(option) => option.label}
renderInput={(param) => (
<TextField
{...param}
label="Search"
variant="outlined"
onChange={(event) => setSearchTermValue(event.target.value)}
/>
)}
onChange={onSelection}
/>
);
}
Hope this code is useful for you.

Related

algolia instantsearch only show result that matches particular query (react)

I'm using react instant search to show list of users in select options. I managed to show all the users in select option but I want to only show users with admin role (role === 'admin').
Ofcourse I can do this in client side by filtering the hits on role but is there
any way to achieve this with react instant search?
This is how I showed all user.
AlgoliaProvider.js
import React from "react";
import algoliasearch from "algoliasearch/lite";
import { InstantSearch } from "react-instantsearch-dom";
const searchClient = algoliasearch(
process.env.REACT_APP_ALGOLIA_APP_ID,
process.env.REACT_APP_ALGOLIA_SEARCH_KEY
);
const AlgoliaProvider = ({ indexName, children, ...rest }) => {
return (
<InstantSearch indexName={indexName} searchClient={searchClient} {...rest}>
{children}
</InstantSearch>
);
};
export default AlgoliaProvider;
AutoCompleteSelect.js
import React from 'react';
import { Select } from 'antd';
import { connectAutoComplete } from 'react-instantsearch-dom';
const { Option } = Select;
const AutoCompleteSelect = ({
hits,
refine,
...rest
}) => {
const handleSuggestion = value => {
refine(value);
};
return (
<Select showSearch onSearch={handleSuggestion} {...rest}>
{hits.map(user => (
<Option
key={user.uid}
value={user.uid}
>
<span>{user.name}</span>
</Option>
))}
</Select>
);
};
export default connectAutoComplete(AutoCompleteSelect);
App.js
<AlgoliaProvider indexName="User">
<AutoCompleteSelect placeholder="user search" />
</AlgoliaProvider>
You need the faceting feature to achieve this.
Read more about it here: https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/
I have configured one such facet via the UI (algolia frontend) and then I use it like this in instantSearch:
<InstantSearch searchClient={.... >
<Configure
hitsPerPage={<Number>}
filters={`role:${user.role}`} />
...
...
</InstantSearch>
See the filters part in the code, 'role' needs to be added as a facet in the configuration (via ui or code).
using Searchstate
<InstantSearch
....
searchState={{
role: 'admin',
}}
}}
More on: https://www.algolia.com/doc/api-reference/widgets/ui-state/react/

React render list only when data source changes

Basically I have a modal with a state in the parent component and I have a component that renders a list. When I open the modal, I dont want the list to re render every time because there can be hundreds of items in the list its too expensive. I only want the list to render when the dataSource prop changes.
I also want to try to avoid using useMemo if possible. Im thinking maybe move the modal to a different container, im not sure.
If someone can please help it would be much appreciated. Here is the link to sandbox: https://codesandbox.io/s/rerender-reactmemo-rz6ss?file=/src/App.js
Since you said you want to avoid React.memo, I think the best approach would be to move the <Modal /> component to another "module"
export default function App() {
return (
<>
<Another list={list} />
<List dataSource={list} />
</>
);
}
And inside <Another /> component you would have you <Modal />:
import React, { useState } from "react";
import { Modal } from "antd";
const Another = ({ list }) => {
const [showModal, setShowModal] = useState(false);
return (
<div>
<Modal
visible={showModal}
onCancel={() => setShowModal(false)}
onOk={() => {
list.push({ name: "drink" });
setShowModal(false);
}}
/>
<button onClick={() => setShowModal(true)}>Show Modal</button>
</div>
)
}
export default Another
Now the list don't rerender when you open the Modal
You can use React.memo, for more information about it please check reactmemo
const List = React.memo(({ dataSource, loading }) => {
console.log("render list");
return (
<div>
{dataSource.map((i) => {
return <div>{i.name}</div>;
})}
</div>
);
});
sandbox here

Run some code in React after multiple async useState setters

I have a functional React component in which I am using useState to manage state. Normally, it's just a form with two fields - code and env - which the user can manually fill out and submit. However, when the component loads, I also want to check any querystring params and if the appropriate ones exist, I want to populate and submit the form automatically. That way, users can bookmark specific form submissions.
The problem I'm having is that, as we all know, useState setters are async, just like setState in class components. As both form fields are controlled by state, setting both values will kick off multiple renders, so where should I put the code to submit the form so that I'm guaranteed that both state updates have completed?
Here is the form:
And here is a simplified, sanitized version of the code I have:
import React, { useState, useEffect } from "react";
import axios from "axios";
import queryString from "query-string";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import ToggleButtonGroup from "react-bootstrap/ToggleButtonGroup";
import ToggleButton from "react-bootstrap/ToggleButton";
import Card from "react-bootstrap/Card";
/*
* this component will show a spinner or the results from the API when complete
*/
const PortalDisplay = ({ data: portal, isLoading }) => {
if (Object.keys(portal).length === 0 && !isLoading) {
return null;
} else if (isLoading) {
return (
<div>
<p>loading…</p>
</div>
);
} else if (!!portal.id && !isLoading) {
return <div className="card-portal">data goes here</div>;
}
};
/*
* main component
*/
const PortalConfiguration = () => {
const [validated, setValidated] = useState(false);
const [code, setCode] = useState("");
const [env, setEnv] = useState("prod");
const [portalInfo, setPortalInfo] = useState({});
const [isLoading, setIsLoading] = useState(false);
const params = queryString.parse(location.search);
const onSubmitForm = (event) => {
const form = event.currentTarget;
setValidated(true);
if (form.checkValidity() === false) {
event.preventDefault();
event.stopPropagation();
return;
}
//re-initialize
setIsLoading(true);
setPortalInfo({});
axios
.get(`http://www.example.com/api`)
.then((response) => {
setIsLoading(false);
setPortalInfo({ ...response.data, ...{ baseUrl: baseUrl[env] } });
})
.catch((error) => console.log(error));
event.preventDefault();
event.stopPropagation();
};
useEffect(() => {
if (!!params && !!params.portal && !!params.env) {
if (!/^[a-zA-Z]{3}$/.test(params.portal) || (params.env !== "prod" && params.env !== "demo")) {
console.log(`Specified portal parameters {portal: ${params.portal}} and {env: ${params.env}} are incorrect`);
} else {
// this is where i want to set the portal and env states and submit the form
}
}
}, [params.portal]);
return (
<>
<h1>Your Portal Configuration</h1>
<Card>
<Card.Body>
<Form noValidate validated={validated} inline className="form-portal" onSubmit={onSubmitForm}>
<Form.Group className="form-group-code">
<label className="sr-only" htmlFor="code">
Portal Code
</label>
<Form.Control
type="text"
id="code"
value={code}
required
placeholder="Enter Portal Code (e.g. 'FWU')"
maxLength="3"
onChange={(e) => setCode(e.target.value)}
/>
<Form.Control.Feedback type="invalid">Portal Code must be three letters (a-z)</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<ToggleButtonGroup type="radio" name="env" value={env} onChange={(val) => setEnv(val)}>
<ToggleButton type="radio" name="env" value="prod" variant="primary" className="btn-inline">
Production
</ToggleButton>
<ToggleButton type="radio" name="env" value="demo" variant="primary" className="btn-inline">
Demo
</ToggleButton>
</ToggleButtonGroup>
</Form.Group>
<Button variant="secondary" block="true" className="btn-inline" type="submit">
Submit
</Button>
</Form>
</Card.Body>
</Card>
<PortalDisplay data={portalInfo} isLoading={isLoading} env={env} />
</>
);
};
export default PortalConfiguration;
The line which is commented out and says "this is where i want to set the portal and env states and submit the form" is where I know I have querystring params passed and need to set both states, then submit the form.
FWIW, I have considered the usual answer to the question of how to deal with useState's asynchronicity, which is to handle it in useEffect, scoped to the particular state variable you are interested in. The two problems with that is that 1) I have two state variables that need to be updated so I don't think there's any guarantee that they will be updated in the order I called the setters, creating a possible race condition, and 2) I don't want to call this code every time that code or env updates, which can happen when the user manually interacts with the form. I only want it to be auto-submitted when the component detects the querystring upon loading.

Material UI + React Form Hook + multiple checkboxes + default selected

I am trying to build a form that accommodates multiple 'grouped' checkboxes using react-form-hook Material UI.
The checkboxes are created async from an HTTP Request.
I want to provide an array of the objects IDs as the default values:
defaultValues: { boat_ids: trip?.boats.map(boat => boat.id.toString()) || [] }
Also, when I select or deselect a checkbox, I want to add/remove the ID of the object to the values of react-hook-form.
ie. (boat_ids: [25, 29, 4])
How can I achieve that?
Here is a sample that I am trying to reproduce the issue.
Bonus point, validation of minimum selected checkboxes using Yup
boat_ids: Yup.array() .min(2, "")
I've been struggling with this as well, here is what worked for me.
Updated solution for react-hook-form v6, it can also be done without useState(sandbox link below):
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const defaultNames = ["bill", "Manos"];
const { control, handleSubmit } = useForm({
defaultValues: { names: defaultNames }
});
const [checkedValues, setCheckedValues] = useState(defaultNames);
function handleSelect(checkedName) {
const newNames = checkedValues?.includes(checkedName)
? checkedValues?.filter(name => name !== checkedName)
: [...(checkedValues ?? []), checkedName];
setCheckedValues(newNames);
return newNames;
}
return (
<form onSubmit={handleSubmit(data => console.log(data))}>
{["bill", "luo", "Manos", "user120242"].map(name => (
<FormControlLabel
control={
<Controller
name="names"
render={({ onChange: onCheckChange }) => {
return (
<Checkbox
checked={checkedValues.includes(name)}
onChange={() => onCheckChange(handleSelect(name))}
/>
);
}}
control={control}
/>
}
key={name}
label={name}
/>
))}
<button>Submit</button>
</form>
);
}
Codesandbox link: https://codesandbox.io/s/material-demo-54nvi?file=/demo.js
Another solution with default selected items done without useState:
https://codesandbox.io/s/material-demo-bzj4i?file=/demo.js
Breaking API changes made in 6.X:
validation option has been changed to use a resolver function wrapper and a different configuration property name
Note: Docs were just fixed for validationResolver->resolver, and code examples for validation in repo haven't been updated yet (still uses validationSchema for tests). It feels as if they aren't sure what they want to do with the code there, and it is in a state of limbo. I would avoid their Controller entirely until it settles down, or use Controller as a thin wrapper for your own form Controller HOC, which appears to be the direction they want to go in.
see official sandbox demo and the unexpected behavior of "false" value as a string of the Checkbox for reference
import { yupResolver } from "#hookform/resolvers";
const { register, handleSubmit, control, getValues, setValue } = useForm({
resolver: yupResolver(schema),
defaultValues: Object.fromEntries(
boats.map((boat, i) => [
`boat_ids[${i}]`,
preselectedBoats.some(p => p.id === boats[i].id)
])
)
});
Controller no longer handles Checkbox natively (type="checkbox"), or to better put it, handles values incorrectly. It does not detect boolean values for checkboxes, and tries to cast it to a string value. You have a few choices:
Don't use Controller. Use uncontrolled inputs
Use the new render prop to use a custom render function for your Checkbox and add a setValue hook
Use Controller like a form controller HOC and control all the inputs manually
Examples avoiding the use of Controller:
https://codesandbox.io/s/optimistic-paper-h39lq
https://codesandbox.io/s/silent-mountain-wdiov
Same as first original example but using yupResolver wrapper
Description for 5.X:
Here is a simplified example that doesn't require Controller. Uncontrolled is the recommendation in the docs. It is still recommended that you give each input its own name and transform/filter on the data to remove unchecked values, such as with yup and validatorSchema in the latter example, but for the purpose of your example, using the same name causes the values to be added to an array that fits your requirements.
https://codesandbox.io/s/practical-dijkstra-f1yox
Anyways, the problem is that your defaultValues doesn't match the structure of your checkboxes. It should be {[name]: boolean}, where names as generated is the literal string boat_ids[${boat.id}], until it passes through the uncontrolled form inputs which bunch up the values into one array. eg: form_input1[0] form_input1[1] emits form_input1 == [value1, value2]
https://codesandbox.io/s/determined-paper-qb0lf
Builds defaultValues: { "boat_ids[0]": false, "boat_ids[1]": true ... }
Controller expects boolean values for toggling checkbox values and as the default values it will feed to the checkboxes.
const { register, handleSubmit, control, getValues, setValue } = useForm({
validationSchema: schema,
defaultValues: Object.fromEntries(
preselectedBoats.map(boat => [`boat_ids[${boat.id}]`, true])
)
});
Schema used for the validationSchema, that verifies there are at least 2 chosen as well as transforms the data to the desired schema before sending it to onSubmit. It filters out false values, so you get an array of string ids:
const schema = Yup.object().shape({
boat_ids: Yup.array()
.transform(function(o, obj) {
return Object.keys(obj).filter(k => obj[k]);
})
.min(2, "")
});
Here is a working version:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import FormControlLabel from "#material-ui/core/FormControlLabel";
import Checkbox from "#material-ui/core/Checkbox";
export default function CheckboxesGroup() {
const { control, handleSubmit } = useForm({
defaultValues: {
bill: "bill",
luo: ""
}
});
return (
<form onSubmit={handleSubmit(e => console.log(e))}>
{["bill", "luo"].map(name => (
<Controller
key={name}
name={name}
as={
<FormControlLabel
control={<Checkbox value={name} />}
label={name}
/>
}
valueName="checked"
type="checkbox"
onChange={([e]) => {
return e.target.checked ? e.target.value : "";
}}
control={control}
/>
))}
<button>Submit</button>
</form>
);
}
codesandbox link: https://codesandbox.io/s/material-demo-65rjy?file=/demo.js:0-932
However, I do not recommend doing so, because Checkbox in material UI probably should return checked (boolean) instead of (value).
Here's my solution, which is not using all the default components from Material UI cause at my interface each radio will have an icon and text, besides the default bullet point not be showed:
const COMPANY = "company";
const INDIVIDUAL = "individual";
const [scope, setScope] = useState(context.scope || COMPANY);
const handleChange = (event) => {
event.preventDefault();
setScope(event.target.value);
};
<Controller
as={
<FormControl component="fieldset">
<RadioGroup
aria-label="scope"
name="scope"
value={scope}
onChange={handleChange}
>
<FormLabel>
{/* Icon from MUI */}
<Business />
<Radio value={COMPANY} />
<Typography variant="body1">Company</Typography>
</FormLabel>
<FormLabel>
{/* Icon from MUI */}
<Personal />
<Radio value={INDIVIDUAL} />
<Typography variant="body1">Individual</Typography>
</FormLabel>
</RadioGroup>
</FormControl>
}
name="scope"
control={methods.control}
/>;
Observation: At this example I use React Hook Form without destruct:
const methods = useForm({...})
This is my solution with react hook form 7, the other solutions don't work with reset or setValue.
<Controller
name={"test"}
control={control}
render={({ field }) => (
<FormControl>
<FormLabel id={"test"}>{"label"}</FormLabel>
<FormGroup>
{items.map((item, index) => {
const value = Object.values(item);
return (
<FormControlLabel
key={index}
control={
<Checkbox
checked={field.value.includes(value[0])}
onChange={() =>
field.onChange(handleSelect(value[0],field.value))
}
size="small"
/>
}
label={value[1]}
/>
);
})}
</FormGroup>
</FormControl>
)}
/>
link to codesandbox: Mui multiple checkbox

Update a component with onChange. React-Hooks

I'm building a dropdown with suggestions that fetch data from an API. The input from the search bar is being stored using setState and it is updated when i change the value in the text input.
The thing is that I'm not managing to update the users lists from the dropdown each time I enter a new character in the text input. Can I somehow force the component to be rendered every time the props change? Or is there a better approach?
import React, {useState, useEffect} from 'react';
import Dropdown from '../Dropdown/Dropdown';
import './SearchBar.css';
// Component created with arrow function making use of hooks
const SearchBar = (props) => {
const [input, setInput] = useState('');
const [dropdownComponent, updateDropdown] = useState(<Dropdown input={input}/>)
useEffect(() => {updateDropdown(<Dropdown input={input}/>)}, [input])
const onChange = (e) => {
setInput(e.currentTarget.value)
updateDropdown(<Dropdown input={input}/>)
console.log("=(")
}
return(
<div className="search-bar">
<input type="text" placeholder={props.placeholder} onChange={onChange}/>
{dropdownComponent}
</div>
)
}
export default SearchBar;
I can't make the problem happen using your code in a simple test, but your onChange does has a problem: It's using input to update the dropdown, but it's not using useCallback to ensure that input isn't stale when it does. Either:
Don't update the dropdown in your onChange, allowing your useEffect callback to do it; or
Use e.target.value instead of input and get rid of the useEffect updating the dropdown; or
Don't memoize the dropdown (e.g., don't put it in state) since you want to update it when the input changes anyway, just render it directly in the JSX
Of those, with what you've shown, #3 is probably the simplest:
const SearchBar = (props) => {
const [input, setInput] = useState('');
const onChange = (e) => {
setInput(e.currentTarget.value);
};
return(
<div className="search-bar">
<input type="text" placeholder={props.placeholder} onChange={onChange}/>
<Dropdown input={input}/>
</div>
);
}
Live Example:
const {useState, useEffect} = React;
function Dropdown({input}) {
return <div>Dropdown for "{input}"</div>;
}
const SearchBar = (props) => {
const [input, setInput] = useState('');
const onChange = (e) => {
setInput(e.currentTarget.value);
};
return(
<div className="search-bar">
<input type="text" placeholder={props.placeholder} onChange={onChange}/>
<Dropdown input={input}/>
</div>
);
}
ReactDOM.render(<SearchBar />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

Categories

Resources