How to hide MUI Autocomplete Popper - javascript

I am trying to make a component which has the option of showing a history of selections or hiding the input. This is because the user may want to hide the data if they are in public but want the convenience of seeing past values if they aren't. I have the following component.
const SensitiveTextField: FunctionComponent<any> = ({ ...props }) => {
const [showData, setShowData] = useState(false);
useEffect(() => {
(async () => {
const storage = new ApplicationStorage();
const showFieldData = props.name
? await storage.getShowSensitiveFieldData(props.name)
: false;
setShowData(showFieldData ?? false);
})();
}, []);
const onVisibilityChange = () => {
setShowData((value) => {
const newValue = !value;
if (props.name) {
const storage = new ApplicationStorage();
storage.setShowSensitiveFieldData(props.name, newValue);
}
return newValue;
});
};
const renderInput = (params: any) => {
return (
<TextField
{...params}
type={showData ? 'text' : 'password'}
InputProps={{
...params.InputProps,
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={onVisibilityChange}
onMouseDown={onVisibilityChange}
>
{showData ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
),
}}
/>
);
};
return (
<Autocomplete {...props} renderInput={(params) => renderInput(params)} />
);
};
so the idea is that when showData is false, the input will be hidden and so will the options. I tried to do this by filtering the options to nothing but there is still an empty component that comes up. I tried to create a custom renderOptions that would just create an empty component but the same small bit showed up. The autocomplete uses the Popper component, is there a way to set that popper component to not render at all inside of the Autocomplete?
Here is where I invoke SensitiveAutocomplete
<SensitiveAutocomplete
name={item.key}
fullWidth
size="small"
onChange={onUserDefineValueChange(item.key)}
label={inputLabel(item.key)}
value={item.value}
options={item.history}
/>

Related

How to make a wrapper interchangable?

I am facing a problem with my formik fields and need to use FastFields in some situations and Fields in others. Therefor my FormikControl, a class for building formik inputs needs to switch between and as wrappers. I am not sure how to do this.
Here is part of the class:
const FormikControl = (props) => {
const {
name,
type,
label,
disabled,
fullWidth,
isDynamic, // Field (true) or FastField
} = props;
const classes = useStyles();
const inputProps = {};
switch (type) {
case 'text':
return (
<>
<ComplexField name={name} isDynamic={isDynamic} {...props}> // meant to be the wrapper that switches
{({ field }) => {
return (
<TextField
{...field}
variant="standard"
label={label}
fullWidth={fullWidth}
disabled={disabled}
InputLabelProps={{
shrink: true,
}}
InputProps={inputProps}
value={field.value || ''}
/>
);
}}
</ComplexField>
<ErrorMessage error name={name} component={FormHelperText} />
</>
);
ComplexField is meant to be the flexable wrapper and needs be able to switch between Field and FastField based on "isDynamic".
My best try was this:
const ComplexField = ({ name, children, isDynamic, ...props }) => {
return isDynamic ? (
<Field name={name} props={props}>{({ field }) => children({ ...props, field })}</Field>
) : (
<FastField name={name} props={props}>{({ field }) => children({ ...props, field })}</FastField>
);
};
Didn't work :(
<ComplexField name={name}>
You're not passing there the prop "isDynamic"
Is it intentional? Because like that it's obvious that doesnt work
<ComplexField name={name} isDynamic={isDynamic}>
If you want to use a better pattern, read that Conditionally render react components

Warning: Encountered two children with the same key, 'Device 846' in Autocomplete of material-ui [duplicate]

I'm trying to create a Material-UI Autocomplete component that essentially just displays search results to the user. Some of the options' names will be duplicates, but they will all have unique IDs. I receive the following warning:
index.js:1 Warning: Encountered two children with the same key, Name B. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
const SearchField = () => {
const [open, setOpen] = React.useState(false)
const [searchQuery, setSearchQuery] = React.useState('')
const [searchResults, setSearchResults] = React.useState([])
const loading = true //later
const debounced = useDebouncedCallback(
async searchQuery => {
if (searchQuery) {
let result = await doSearch(searchQuery)
if (result.status === 200) {
setSearchResults(result.data)
} else {
console.error(result)
}
}
},
1000
)
const handleInputChange = e => {
if (e.target.value && e.target.value !== searchQuery) {
debounced(e.target.value)
setSearchQuery(e.target.value)
}
}
const options = [{
name: 'Name A',
id: 'entry_0597856'
},{
name: 'Name B',
id: 'entry_3049854'
},{
name: 'Name B',
id: 'entry_3794654'
},{
name: 'Name C',
id: 'entry_9087345'
}]
return (
<Autocomplete
id='search_freesolo'
freeSolo
selectOnFocus
clearOnBlur
handleHomeEndKeys
autoHighlight
onInputChange={handleInputChange}
open={true}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
loading={loading}
key={option => option.id}
options={options}
getOptionLabel={option => option.name}
renderOption={(props, option) => (
<Box
component='li'
{...props}
>
{option.name}
</Box>
)}
renderInput={params => {
return (
<TextField
{...params}
required
id="search_bar"
label="Search"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress size={18} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
}
/>
)
}
You can define your own renderOption that can return the list item with a correct key value. Your code complains about the duplicated keys because by default, Autocomplete uses the getOptionLabel(option) to retrieve the key:
<Autocomplete
renderOption={(props, option) => {
return (
<li {...props} key={option.id}>
{option.name}
</li>
);
}}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
If it still doesn't work, check your props order, you need to declare the key prop last, if you put it before the props provided by the callback:
<Box component='li' key={key} {...props}
Then it will be overridden by the props.key from MUI. It should be like this:
<Box component='li' {...props} key={key}
Live Demo

Select all and Select None buttons in Autocomplete Material UI React

I want to implement two buttons Select All and Select None inside Autocomplete React Material UI along with checkbox for each option.When Select All button is clicked all the options must be checked and when I click Select None all the options must be unchecked.
How do I implement that ?
<Autocomplete
id={id }
size={size}
multiple={multiple}
value={value}
disabled={disabled}
options={items}
onChange={handleChange}
getOptionLabel={option => option.label}
renderOption={(option, { selected }) => (
<React.Fragment >
{isCheckBox(check, selected)}
{option.label}
</React.Fragment>
)}
renderInput={params => (
<TextField id="dropdown_input"
{...params} label="controlled" variant={variant} label={label} placeholder={placeholder} />
)}
/>
export function isCheckBox(check, selected) {
if (check) {
const CheckBox = <Checkbox
id="dropdown_check"
icon={icon}
checkedIcon={checkedIcon}
checked={selected}
/>
return CheckBox;
}
return null;
}
I stumbled into the same issue earlier today.
The trick is to use local state to manage what has been selected, and change the renderOption to select * checkboxes if the local state has the 'all' key in it.
NB: At the time of writing React 16 is what I'm working with
I'm on a deadline, so I'll leave a codesandbox solution for you instead of a rushed explanation. Hope it helps :
Select All AutoComplete Sandbox
Updated
for React version 16.13.1 and later. codesandbox
const [open, setOpen] = useState(false);
const timer = useRef(-1);
const setOpenByTimer = (isOpen) => {
clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
setOpen(isOpen);
}, 200);
}
const MyPopper = function (props) {
const addAllClick = (e) => {
clearTimeout(timer.current);
console.log('Add All');
}
const clearClick = (e) => {
clearTimeout(timer.current);
console.log('Clear');
}
return (
<Popper {...props}>
<ButtonGroup color="primary" aria-label="outlined primary button group">
<Button color="primary" onClick={addAllClick}>
Add All
</Button>
<Button color="primary" onClick={clearClick}>
Clear
</Button>
</ButtonGroup>
{props.children}
</Popper>
);
};
return (
<Autocomplete
PopperComponent={MyPopper}
onOpen={(e) => {
console.log('onOpen');
setOpenByTimer(true);
}}
onClose={(obj,reason) => {
console.log('onClose', reason);
setOpenByTimer(false);
}}
open={open}
.....
....
/>
);
Old Answer
Just customise PopperComponent and do whatever you want.
Autocomplete API
const addAllClick = (e: any) => {
setValue(items);
};
const clearClick = (e: any) => {
setValue([]);
};
const MyPopper = function (props: any) {
return (
<Popper {...props}>
<ButtonGroup color="primary" aria-label="outlined primary button group">
<Button color="primary" onClick={addAllClick}>
Add All
</Button>
<Button color="primary" onClick={clearClick}>
Clear
</Button>
</ButtonGroup>
{props.children}
</Popper>
);
};
<Autocomplete
PopperComponent={MyPopper}
...
/>
If you want to make an autocomplete with select all option using react material ui and react hook form, you can implement to Autocomplete like so
multiple: To allow multiple selection
disableCloseOnSelect: To disable the close of the box after each selection
options: Array of items of selection
value: Selected options.
getOptionLabel: The string value of the option in our case is name
filterOptions: A function that determines the filtered options to be rendered on search, in our case we used it to add selectAll checkbox.
renderOption: Render the option, use getOptionLabel by default.
renderInput: To render the input,
onChange: Callback fired when the value changes
Now you can play with selected values using handleChange so once the select is fired check if the selected option is select all if yes then set the newest selectedOptions
<Autocomplete
multiple
disableCloseOnSelect
options={items}
value={selectedOptions}
getOptionLabel={(option) => option.name}
filterOptions={(options, params) => {
const filtered = filter(options, params)
return [{ id: 0, name: selectAllLabel }, ...filtered]
}}
renderOption={(props, option, { selected }) => {
// To control the state of 'select-all' checkbox
const selectAllProps =
option.name === 'Sélectionner Tous' ? { checked: allSelected } : {}
return (
<li {...props}>
<Checkbox checked={selected} {...selectAllProps} />
{option.name}
</li>
)
}}
renderInput={(params) => (
<TextField {...params} label={label} placeholder={label} />
)}
onChange={handleChange}
/>
you can refer to the Autocomplete API to get detailed definition of each item
You can refer to this codeSendBox to check a demo of react material Autocomplete with select all using react material ui version 5 and react hook form verion 7

Reactjs - Update options by hitting API on every input change in Autocomplete component (Material UI)

I am a beginner in Reactjs. I want to create an Autocomplete component where on every input change the API is hit and the options are updated accordingly. I am using the Autocomplete component provided by Material UI. As I understand, the example given here hits an API once and filters it locally. I tried using the InputChange props provided by material component. Also I found this anser - https://stackoverflow.com/a/59751227/8090336. But can't figure out the right way.
import Autocomplete from "#material-ui/lab/Autocomplete";
import TextField from "#material-ui/core/TextField";
import {CircularProgress} from "#material-ui/core";
import debounce from 'lodash/debounce';
const SelectField = ({inputLabel}) => {
const [ open, setOpen ] = React.useState(false);
const [ options, setOptions ] = React.useState([]);
const [ inputValue, setInputValue ] = React.useState("");
const loading = open && options.length === 0;
const onInputChange = debounce((event, value) => {
console.log("OnInputChange",value);
setInputValue(value);
(async() => {
const response = await fetch('https://api.tvmaze.com/search/shows?q='+inputValue);
console.log("API hit again")
let movies = await response.json();
if(movies !== undefined) {
setOptions(movies);
console.log(movies)
}
})();
}, 1500);
return (
<Autocomplete
style={{ width:300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionLabel={(option) => option.show.name}
onInputChange={onInputChange}
options={options}
loading={loading}
renderInput={(params) => (<TextField
{...params}
label={inputLabel}
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} />: null }
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
}
export default SelectField;
I had faced this problem I manually called the API when ever the user types in. find the link for the sandbox. Check the onChange prop for the textfield rendered inside the autocomplete
// *https://www.registers.service.gov.uk/registers/country/use-the-api*
import fetch from "cross-fetch";
import React from "react";
import TextField from "#material-ui/core/TextField";
import Autocomplete from "#material-ui/lab/Autocomplete";
import CircularProgress from "#material-ui/core/CircularProgress";
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const loading = open && options.length === 0;
const onChangeHandle = async value => {
// this default api does not support searching but if you use google maps or some other use the value and post to get back you reslut and then set it using setOptions
console.log(value);
const response = await fetch(
"https://country.register.gov.uk/records.json?page-size=5000"
);
const countries = await response.json();
setOptions(Object.keys(countries).map(key => countries[key].item[0]));
};
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.name === value.name}
getOptionLabel={option => option.name}
options={options}
loading={loading}
renderInput={params => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
onChange={ev => {
// dont fire API if the user delete or not entered anything
if (ev.target.value !== "" || ev.target.value !== null) {
onChangeHandle(ev.target.value);
}
}}
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? (
<CircularProgress color="inherit" size={20} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
);
}

React JS Material UI Autocomplete: Change Options

I want to use an Autocomplete field for my React JS Project. For the design of the UI I use Material UI. In the documentation you can see the following example:
<Autocomplete
required
id="combo-box-demo"
filterOptions={(x) => x}
value={this.state.departure}
options={top100Films}
getOptionLabel={(option) => option.title}
renderInput={(params) => <TextField {...params} label="Startpunkt" variant="outlined" />}
/>
The options objects have the following default value:
let top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'Monty Python and the Holy Grail', year: 1975 },
];
For my purpose I want to dynamically change the options since I use an Rest API where I get the results for the input. My question is therefore how I can change the options dynamically when the user is typing.
You can use onInputChange prop in your case:
<Autocomplete
required
id='combo-box-demo'
filterOptions={(x) => x}
value={this.state.departure}
options={top100Films}
getOptionLabel={(option) => option.title}
onInputChange={(event: object, value: string, reason: string) => {
if (reason === 'input') {
changeOptionBaseOnValue(value);
}
}}
renderInput={(params) => (
<TextField {...params} label='Startpunkt' variant='outlined' />
)}
/>
Then you can define changeOptionBaseOnValue to handle your options.
You can check this example:
import fetch from 'cross-fetch';
import React from 'react';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import CircularProgress from '#material-ui/core/CircularProgress';
function sleep(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
export default function Asynchronous() {
const [open, setOpen] = React.useState(false);
const [options, setOptions] = React.useState([]);
const loading = open && options.length === 0;
React.useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
const response = await fetch('https://country.register.gov.uk/records.json?page-size=5000');
await sleep(1e3); // For demo purposes.
const countries = await response.json();
if (active) {
setOptions(Object.keys(countries).map((key) => countries[key].item[0]));
}
})();
return () => {
active = false;
};
}, [loading]);
React.useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="asynchronous-demo"
style={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
getOptionSelected={(option, value) => option.name === value.name}
getOptionLabel={(option) => option.name}
options={options}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
label="Asynchronous"
variant="outlined"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
)}
/>
);
}
Source
I'm doing this as part of an address search/verification by using the OnChange in the text field with a handleAddressChange function that calls a findAddresses function. findAddresses uses Axios to make a call to an API, and then saves those results and displays them as the options for the results in the autocomplete.
Here's a simplified version of my code:
import React, { useState, ChangeEvent } from 'react';
import {
TextField,
InputAdornment
} from "#material-ui/core";
import Autocomplete from '#material-ui/lab/Autocomplete';
import { Search } from "#material-ui/icons";
import axios from "axios";
const AddressSearch = (props) => {
const [addressesList, setAddressesList] = useState([]);
const [inputAddress, setInputAddress] = useState<string>("");
const handleAddressChange = (event: ChangeEvent<{ value: unknown }>) => {
setInputAddress(event.target.value as string);
findAddresses(event.target.value as string);
};
const baseUrl = 'https://api.website.com/';
const findAddresses = (text?: string) => {
let params = `Key=value`
if (!!text) {
params += (`&Text=` + text);
let addressesResponse;
return (
axios.get(baseUrl + params)
.then(response => {
addressesResponse = response.data.Items
if (!Array.isArray(addressesResponse) || !addressesResponse.length) {
return;
}
setAddressesList(addressesResponse);
})
.catch(error => console.log(error))
)
}
}
return (
<div>
<Autocomplete
id="address-autocomplete"
freeSolo
options={addressesList}
getOptionLabel={(option) => option.Text}
popupIcon={<Search />}
renderInput={(params) => <TextField
id="address-input"
{...params}
onChange={handleAddressChange}
placeholder="Quickly find your address"
InputProps={{ ...params.InputProps,
startAdornment: (
<InputAdornment position="start"><Search /></InputAdornment>
)
}}
/> }
/>
</div>
);
}
export default AddressSearch;

Categories