My autocomplete component is pulling a list of books from an API. I am rendering them as options in the Autocomplete component, and also outputting them as a list at the bottom of the page for debugging purposes. Also outputting the JSON from the API.
Two issues seem to be intertwined. First, the Autocomplete options don't seem to be all rendering. There are up to 10 results (limited to 10 by the API call) and they're all rending in the list below the autocomplete, but not in the list of options in the Autocomplete. Second, when the API is being called (like the time between changing the text from "abc" to "abcd") it shows "No options" rather than displaying the options from just "abc".
In the sandbox code here try typing slowly - 1 2 3 4 5 6 - you'll see that there are results in the <ul> but not in the <Autocomplete>.
Any ideas on why this (or maybe both separately) are happening?
Thanks!
Code from sandbox:
import React, { useState, useEffect } from "react";
import Autocomplete from "@material-ui/lab/Autocomplete";
import {
makeStyles,
Typography,
Popper,
InputAdornment,
TextField,
Card,
CardContent,
CircularProgress,
Grid,
Container
} from "@material-ui/core";
import MenuBookIcon from "@material-ui/icons/MenuBook";
import moment from "moment";
// sample ISBN: 9781603090254
function isbnMatch(isbn) {
const str = String(isbn).replace(/[^0-9a-zA-Z]/, ""); // strip out everything except alphanumeric
const r = /^[0-9]{13}$|^[0-9]{10}$|^[0-9]{9}[Xx]$/; // set the regex for 10, 13, or 9+X characters
return str.match(r);
// return str.match(/^[0-9]{3}$|^[0-9]{3}$|^[0-9]{2}[Xx]$/);
}
const useStyles = makeStyles((theme) => ({
adornedEnd: {
backgroundColor: "inherit",
height: "2.4rem",
maxHeight: "3rem"
},
popper: {
maxWidth: "fit-content"
}
}));
export default function ISBNAutocomplete() {
console.log(`Starting ISBNAutocomplete`);
const classes = useStyles();
const [options, setOptions] = useState([]);
const [inputText, setInputText] = useState("");
const [open, setOpen] = useState(false);
const loading = open && options.length === 0 && inputText.length > 0;
useEffect(() => {
async function fetchData(searchText) {
const isbn = isbnMatch(searchText);
//console.log(`searchText = ${searchText}`);
//console.log(`isbnMatch(searchText) = ${isbn}`);
const fetchString = `https://www.googleapis.com/books/v1/volumes?maxResults=10&q=${
isbn ? "isbn:" + isbn : searchText
}&projection=full`;
//console.log(fetchString);
const res = await fetch(fetchString);
const json = await res.json();
//console.log(JSON.stringify(json, null, 4));
json && json.items ? setOptions(json.items) : setOptions([]);
}
if (inputText?.length > 0) {
// only search the API if there is something in the text box
fetchData(inputText);
} else {
setOptions([]);
setOpen(false);
}
}, [inputText, setOptions]);
const styles = (theme) => ({
popper: {
maxWidth: "fit-content",
overflow: "hidden"
}
});
const OptionsPopper = function (props) {
return <Popper {...props} style={styles.popper} placement="bottom-start" />;
};
console.log(`Rendering ISBNAutocomplete`);
return (
<>
<Container>
<h1>Autocomplete</h1>
<Autocomplete
id="isbnSearch"
options={options}
open={open}
//noOptionsText=""
style={{ width: 400 }}
PopperComponent={OptionsPopper}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(event, value) => {
console.log("ONCHANGE!");
console.log(`value: ${JSON.stringify(value, null, 4)}`);
}}
onMouseDownCapture={(event) => {
event.stopPropagation();
console.log("STOPPED PROPAGATION");
}}
onInputChange={(event, newValue) => {
// text box value changed
//console.log("onInputChange start");
setInputText(newValue);
// if ((newValue).length > 3) { setInputText(newValue); }
// else { setOptions([]); }
//console.log("onInputChange end");
}}
getOptionLabel={(option) =>
option.volumeInfo && option.volumeInfo.title
? option.volumeInfo.title
: "Unknown Title"
}
getOptionSelected={(option, value) => option.id === value.id}
renderOption={(option) => {
console.log(`OPTIONS LENGTH: ${options.length}`);
return (
<Card>
<CardContent>
<Grid container>
<Grid item xs={4}>
{option.volumeInfo &&
option.volumeInfo.imageLinks &&
option.volumeInfo.imageLinks.smallThumbnail ? (
<img
src={option.volumeInfo.imageLinks.smallThumbnail}
width="50"
height="50"
/>
) : (
<MenuBookIcon size="50" />
)}
</Grid>
<Grid item xs={8}>
<Typography variant="h5">
{option.volumeInfo.title}
</Typography>
<Typography variant="h6">
(
{new moment(option.volumeInfo.publishedDate).isValid()
? new moment(option.volumeInfo.publishedDate).format(
"yyyy"
)
: option.volumeInfo.publishedDate}
)
</Typography>
</Grid>
</Grid>
</CardContent>
</Card>
);
}}
renderInput={(params) => (
<>
<TextField
{...params}
label="ISBN - 10 or 13 digit"
//"Search for a book"
variant="outlined"
value={inputText}
InputProps={{
...params.InputProps, // make sure the "InputProps" is same case - not "inputProps"
autoComplete: "new-password", // forces no auto-complete history
endAdornment: (
<InputAdornment
position="end"
color="inherit"
className={classes.adornedEnd}
>
<>
{loading ? (
<CircularProgress color="secondary" size={"2rem"} />
) : null}
</>
{/* <>{<CircularProgress color="secondary" size={"2rem"} />}</> */}
</InputAdornment>
),
style: {
paddingRight: "5px"
}
}}
/>
</>
)}
/>
<ul>
{options &&
options.map((item) => (
<li key={item.id}>{item.volumeInfo.title}</li>
))}
</ul>
<span>
inputText: <pre>{inputText && inputText}</pre>
</span>
<span>
<pre>
{options && JSON.stringify(options, null, 3).substr(0, 500)}
</pre>
</span>
<span>Sample ISBN: 9781603090254</span>
</Container>
</>
);
}
