Add autocomplete with multiple and creatable reactjs material ui - reactjs

I want the users to be able to select multiple tags while also allowing them to add a tag if it does not exist, the examples on the material UI documentation work on the freeSolo option which works on string / object values as options whereas when we use multiple, that changes to an array
How do I implement a multiple creatable with material-ui?
My code:
// Fetch Adding tag list
const [listOpen, setListOpen] = useState(false);
const [options, setOptions] = useState<Tag[]>([]);
const loadingTags = listOpen && options.length === 0;
useEffect(() => {
let active = true;
if (!loadingTags) {
return undefined;
}
(async () => {
try {
const response = await getAllTagsForUser();
if (active) {
setOptions(response.data);
}
} catch (error) {
console.log(error);
}
})();
return () => {
active = false;
};
}, [loadingTags]);
useEffect(() => {
if (!listOpen) {
setOptions([]);
}
}, [listOpen]);
<Autocomplete
multiple
id="tags"
open={listOpen}
onOpen={() => {
setListOpen(true);
}}
onClose={() => {
setListOpen(false);
}}
options={options}
disableCloseOnSelect
getOptionLabel={(option) => option?.name || ""}
defaultValue={
contact?.tags?.map((element) => {
return { name: element };
}) || undefined
}
renderOption={(option, { selected }) => (
<React.Fragment>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.name}
</React.Fragment>
)}
style={{ width: 500 }}
renderInput={(params) => (
<TextField {...params} variant="outlined" label="Tags" />
)}
/>;
This is just fetching tags from the server and showing them as options, I understand that to be able to allow adding more, I would need to add filterOptions and onChange but, can someone please provide an example on how to deal with array there?

I know this isn't an quick answer but may someone else could use it. Found this Question buy searching an solution. Didn't find one so I tryed myself and this is what I Created and seems it works.
Based on the original Docs https://mui.com/components/autocomplete/#creatable
Complete example:
import React, { useEffect, useState } from "react";
//Components
import TextField from "#mui/material/TextField";
import Autocomplete, { createFilterOptions } from "#mui/material/Autocomplete";
//Icons
const filter = createFilterOptions();
export default function AutocompleteTagsCreate() {
const [selected, setSelected] = useState([])
const [options, setOptions] = useState([]);
useEffect(() => {
setOptions(data);
}, [])
return (
<Autocomplete
value={selected}
multiple
onChange={(event, newValue, reason, details) => {
let valueList = selected;
if (details.option.create && reason !== 'removeOption') {
valueList.push({ id: undefined, name: details.option.name, create: details.option.create });
setSelected(valueList);
}
else {
setSelected(newValue);
}
}}
filterSelectedOptions
filterOptions={(options, params) => {
const filtered = filter(options, params);
const { inputValue } = params;
// Suggest the creation of a new value
const isExisting = options.some((option) => inputValue === option.name);
if (inputValue !== '' && !isExisting) {
filtered.push({
name: inputValue,
label: `Add "${inputValue}"`,
create: true
});
}
return filtered;
}}
selectOnFocus
clearOnBlur
handleHomeEndKeys
id="tags-Create"
options={options}
getOptionLabel={(option) => {
// Value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
// Add "xxx" option created dynamically
if (option.label) {
return option.name;
}
// Regular option
return option.name;
}}
renderOption={(props, option) => <li {...props}>{option.create ? option.label : option.name}</li>}
freeSolo
renderInput={(params) => (
<TextField {...params} label="Tags" />
)}
/>
);
}
const data = [
{
id: 1,
name: 'Tag1'
},
{
id: 2,
name: 'Tag2'
},
{
id: 3,
name: 'Tag3'
},
{
id: 4,
name: 'Tag4'
},
]

Related

How to use asynchronous autocomplete MUI

I tried to list search result onTyping on the autocomplete using useSearch hook.
I have two problems here.
loads searchdata to options only after a rerender(does not mutate)
encountered multiple child with same key
enter image description here
only adds searchdata to options after a rerender
enter image description here
Here is the image of the search result after a rerender
enter image description here
SearchAutocomplete.jsx
import * as React from 'react'
import TextField from '#mui/material/TextField'
import Autocomplete from '#mui/material/Autocomplete'
import useSearch from 'data/search/useSearch'
import { useHistory } from 'react-router-dom'
export default function SearchAutocomplete() {
const [open, setOpen] = React.useState(false)
const [options, setOptions] = React.useState([])
const [inputValue, setInputvalue] = React.useState([])
const [loading, setLoading] = React.useState(false)
const { isLoadingInitialData, searchdata } = useSearch(inputValue)
const history = useHistory()
console.log(searchdata, 'searchdata')
console.log(opptions, 'options')
React.useEffect(() => {
if (isLoadingInitialData) {
setLoading(true)
} else {
setOptions([...searchdata?.[0]?.data])
}
}, [loading, inputValue])
React.useEffect(() => {
if (!open) {
setLoading(false)
setOptions([])
}
}, [open])
return (
<Autocomplete
id="asynchronous-demo"
sx={{ width: 300 }}
open={open}
onOpen={() => {
setOpen(true)
}}
onClose={() => {
setOpen(false)
}}
filterOptions={(x) => x}
isOptionEqualToValue={(option, value) => option?.title === value?.title}
getOptionLabel={(option) => (option?.title ? option?.title : '')}
options={options}
loading={loading}
onChange={(event, value) => {
history.push(value?.route)
}}
onInputChange={(event, newInputValue) => {
setLoading(true)
setInputvalue(newInputValue)
}}
renderOption={(props, option) => {
return (
<li {...props} key={option.id}>
{option.title}
</li>
)
}}
renderInput={(params) => (
<TextField
{...params}
label="Search ..."
InputProps={{
...params.InputProps,
endAdornment: <React.Fragment>{params.InputProps.endAdornment}</React.Fragment>
}}
/>
)}
/>
)
}
useSearch.js
export default function useSearch(searchValue) {
let url = urls.search
const search = searchValue || ''
const getKey = (pageIndex) => {
return `${url}?search=${search}`
}
const { data, error, size, setSize, mutate } = useSWRInfinite(getKey, fetcher, {
shouldRetryOnError: false
})
const searchdata = data ? [].concat(...data) : []
const isLoadingInitialData = !data && !error
const isLoadingMore =
isLoadingInitialData || (size > 0 && data && typeof data[size - 1] === 'undefined')
const isEmpty = data?.[0]?.data?.length === 0
return {
isLoadingInitialData,
isLoadingMore,
error,
searchdata,
mutate,
setSize,
size
}
}
I tried to get the search results as options on MUI Autocomplete. Created a state.
const [inputValue, setInputvalue] = React.useState([])
and set the value on onInputChange
onInputChange={(event, newInputValue) => {
setInputvalue(newInputValue)
}}
After getting the value on searchdata
const { isLoadingInitialData, searchdata } = useSearch(inputValue)
only adds data on rerender
React.useEffect(() => {
if (isLoadingInitialData) {
setLoading(true)
} else {
setOptions([...searchdata?.[0]?.data])
}
}, [loading, inputValue])
How do I set searchdata to options without rerendering ?
How do I fix multiple child with same key error ?

How to make data persist on refresh React JS?

I have a code where I mount a table with some firebase data but for some reason the values disappear and I been struggling for the next 2 weeks trying to solve this issue I haven't found a solution to this and I have asked twice already and I have try everything so far but it keeps disappearing.
Important Update
I just want to clarify the following apparently I was wrong the issue wasn't because it was a nested collection as someone mentioned in another question. The issue is because my "user" is getting lost in the process when I refresh.
I bring the user from the login to the app like this:
<Estudiantes user={user} />
and then I receive it as a props
function ListadoPedidos({user})
but is getting lost and because is getting lost when I try to use my firebase as:
estudiantesRef = db.collection("usuarios").doc(user.uid).collection("estudiantes")
since the user is "lost" then the uid will be null. Since is null it will never reach the collection and the docs.
I have a simple solution for you. Simply raise the parsing of localStorage up one level, passing the preloadedState into your component as a prop, and then using that to initialize your state variable.
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
Then initialize state with the prop
const initialState = props.preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
And finally, update the useEffect hook to persist state any time it changes.
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
Full Code
import React, { useState, useEffect } from 'react';
import { db } from './firebase';
import { useHistory } from 'react-router-dom';
import './ListadoEstudiantes.css';
import {
DataGrid,
GridToolbarContainer,
GridToolbarFilterButton,
GridToolbarDensitySelector,
} from '#mui/x-data-grid';
import { Button, Container } from '#material-ui/core';
import { IconButton } from '#mui/material';
import PersonAddIcon from '#mui/icons-material/PersonAddSharp';
import ShoppingCartSharpIcon from '#mui/icons-material/ShoppingCartSharp';
import DeleteOutlinedIcon from '#mui/icons-material/DeleteOutlined';
import { Box } from '#mui/system';
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
const Listado = ({ user, preloadedState }) => {
const history = useHistory('');
const crearEstudiante = () => {
history.push('/Crear_Estudiante');
};
const initialState = preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
const parseData = {
pathname: '/Crear_Pedidos',
data: estudiantesData,
};
const realizarPedidos = () => {
if (estudiantesData == 0) {
window.alert('Seleccione al menos un estudiante');
} else {
history.push(parseData);
}
};
function CustomToolbar() {
return (
<GridToolbarContainer>
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
);
}
const [estudiantes, setEstudiantes] = useState([]);
const [selectionModel, setSelectionModel] = useState([]);
const columns = [
{ field: 'id', headerName: 'ID', width: 100 },
{ field: 'nombre', headerName: 'Nombre', width: 200 },
{ field: 'colegio', headerName: 'Colegio', width: 250 },
{ field: 'grado', headerName: 'Grado', width: 150 },
{
field: 'delete',
width: 75,
sortable: false,
disableColumnMenu: true,
renderHeader: () => {
return (
<IconButton
onClick={() => {
const selectedIDs = new Set(selectionModel);
estudiantes
.filter((x) => selectedIDs.has(x.id))
.map((x) => {
db.collection('usuarios')
.doc(user.uid)
.collection('estudiantes')
.doc(x.uid)
.delete();
});
}}
>
<DeleteOutlinedIcon />
</IconButton>
);
},
},
];
const deleteProduct = (estudiante) => {
if (window.confirm('Quiere borrar este estudiante ?')) {
db.collection('usuarios').doc(user.uid).collection('estudiantes').doc(estudiante).delete();
}
};
useEffect(() => {}, [estudiantesData]);
const estudiantesRef = db.collection('usuarios').doc(user.uid).collection('estudiantes');
useEffect(() => {
estudiantesRef.onSnapshot((snapshot) => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes);
});
}, []);
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
return (
<Container fixed>
<Box mb={5} pt={2} sx={{ textAlign: 'center' }}>
<Button
startIcon={<PersonAddIcon />}
variant="contained"
color="primary"
size="medium"
onClick={crearEstudiante}
>
Crear Estudiantes
</Button>
<Box pl={25} pt={2} mb={2} sx={{ height: '390px', width: '850px', textAlign: 'center' }}>
<DataGrid
rows={estudiantes}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
components={{
Toolbar: CustomToolbar,
}}
checkboxSelection
//Store Data from the row in another variable
onSelectionModelChange={(id) => {
setSelectionModel(id);
const selectedIDs = new Set(id);
const selectedRowData = estudiantes.filter((row) => selectedIDs.has(row.id));
setEstudiantesData(selectedRowData);
}}
{...estudiantes}
/>
</Box>
<Button
startIcon={<ShoppingCartSharpIcon />}
variant="contained"
color="primary"
size="medium"
onClick={realizarPedidos}
>
Crear pedido
</Button>
</Box>
</Container>
);
};
I suspect that it's because this useEffect does not have a dependency array and is bring run on every render.
useEffect (() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
})
Try adding a dependency array as follows:
useEffect (() => {
if (estudiantes && estudiantes.length>0)
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
},[estudiantes])
This will still set the localStorage to [] when it runs on the first render. But when the data is fetched and estudiantes is set, the localStorage value will be updated. So I've added a check to check if it's not the empty array.
Change the dependency array of this useEffect to []:
estudiantesRef.onSnapshot(snapshot => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes)
})
}, []);
The data flow in your code is somewhat contradictory, so I modify your code, and it works fine.
You can also try delete or add button, it will modify firebase collection, then update local data.
You can click refresh button in codesandbox previewer (not browser) to observe the status of data update.
Here is the code fargment :
// Set value of `localStorage` to component state if it exist.
useEffect(() => {
const localStorageEstData = window.localStorage.getItem("estudiantes");
localStorageEstData && setEstudiantes(JSON.parse(localStorageEstData));
}, []);
// Sync remote data from firebase to local component data state.
useEffect(() => {
// Subscribe onSnapshot
const unSubscribe = onSnapshot(
collection(db, "usuarios", user.id, "estudiantes"),
(snapshot) => {
const remoteDataSource = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}));
console.info(remoteDataSource);
setEstudiantes(remoteDataSource);
}
);
return () => {
//unSubscribe when component unmount.
unSubscribe();
};
}, [user.id]);
// when `estudiantes` state update, `localStorage` will update too.
useEffect(() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes));
}, [estudiantes]);
Here is the full code sample :
Hope to help you :)

Radio buttons not toggling, checked or highlighted

import React, { useState, useEffect, Fragment } from "react";
import Button from "../../../Resources/Forms/Button";
import Switch from "../../../Resources/Forms/Switch";
import { POST } from "../../../Utils/api";
import Radio from "../../../Resources/Forms/Radio";
import withAppData from "../../../HOC/withAppData";
const InventorySettings = (props) => {
const [state, setState] = useState({});
const [isSaving, setIsSaving] = useState();
const [isStockRequestChecked, setIsStockRequestChecked] = useState(false);
const getStatus = id => {
return props.context.isSettingsActivated(id) ? 1 : 0;
};
const setBusinessSettings = async () => {
const defaultSettings = [
{ state: "enableStockRequest", id: 53 },
{ state: "connectWarehousePickStock", id: 52 },
{ state: "approveRequestOtp", id: 51 },
{ state: "approveRequestManually", id: 50 }
];
for (const setting of defaultSettings) {
await setState({ [setting.state]: getStatus(setting.id) });
}
};
function chooseApprovalMethod(methodType) {
const currentValue = state[methodType];
setState({[methodType]
: currentValue === 1 ? 0: 1})
}
async function saveApprovalMethod() {
setIsSaving(true)
const approvalSettings = [{text:"approvalRequestManually", id: 51}, {text:"approveRequestOtp", id: 50}]
for(const el of approvalSettings) {
const currentValue = state[el.text];
const data = {
settingId: el.id,
status: currentValue
}
await POST(`Common/AddBusinessSetting`, data);
}
setIsSaving(false);
props.context.getBusinessSettings();
}
const updateBasicSettings = async (id, key) => {
setState({ [key]: !state[key] ? 1 : 0 });
const data = {
SettingId: id,
Status: state[key],
};
await POST(`Common/AddBusinessSetting`, data);
props.context.getBusinessSettings();
};
useEffect(() => {
setBusinessSettings();
}, []);
return (
<Fragment>
<div className="basic-settings-section">
<Switch
label={"Connect Warehouse Stock to pick stock"}
light={true}
checked={state && state.connectWarehousePickStock === 1}
onChange={() => updateBasicSettings(52, "connectWarehousePickStock")}
></Switch>
</div>
<div className="basic-settings-section">
<Switch
label={"Stock Request"}
light={true}
checked={isStockRequestChecked}
onChange={() => setIsStockRequestChecked(!isStockRequestChecked)}
></Switch>
{isStockRequestChecked && (
<div className="basic-settings-plan-generate">
<div
className="form__label"
style={{ padding: "2px", marginBottom: "20px" }}
>
<p>Please choose an approval method</p>
</div>
<Radio
label={"Manual Approval"}
name="approval"
value="50"
id="50"
checked={state && state.approveRequestManually === 1}
// onChange={() => (chooseApprovalMethod)}
/>
<Radio
label={"OTP Approval"}
name="approval"
value="51"
id="51"
checked={state && state.approveRequestOtp === 1}
// onChange={() => (chooseApprovalMethod)}
/>
<div className="password-settings-btn"
// onClick={props.context.showToast}
>
<Button
type={"outline"}
size={"medium"}
text={"Save"}
disabled={!state.approveRequestOtp && !state.approveRequestManually}
withMargin={false}
loading={isSaving}
onClick={saveApprovalMethod}
></Button>
</div>
</div>
)}
</div>
</Fragment>
);
}
export default withAppData(InventorySettings);
I added the chooseApprovalMethod function to the radio buttons but still I wasn't getting it well. So I had to call there state using state.text is equal to 1. Please help me out I don't think I know what I'm doing anymore.
Please above are my code, the radio buttons aren't checking or highlighting, so I want them to be checked when clicked on, and I want there ids to be saved when clicking on the save button.
So please guys help me out, as I don't understand it anymore.

How do i select all checkboxes in Javascript?

I am a beginner with javscript So i will be thankful for explanation.
{isolate_list.map((row) => {
return (
<FormControlLabel
control={
<Checkbox
color="primary"
checked={!!checked}
onChange={toggleCheckbox}
name="checkedA"
>
{" "}
</Checkbox>
}
label={row.isolatename}
>
{""}
</FormControlLabel>
);
})}
and i have this button
<Button
onClick={selectall}
style={{ margin: 50 }}
variant="outlined"
label="SELECT ALL ISOLATES"
>
SELECT ALL ISOLATES
</Button>
Can anyone help how can i use the button to select all checkboxes and in the same time i can select every checkbox alone by clicking on it?
I beginn with this part but i am not sure
const [checked, setChecked] = React.useState(true);
const toggleCheckbox = (event) => {
setChecked(event.target.checked);
};
You should hold checkbox value's in the and give the state value as a property to each. For example
<Checkbox
color="primary"
onChange={toggleCheckbox}
name="checkedA"
value={checked}
>
And then in the onClick function
setChecked();
The simplest implementations(without any form manager):
Declare state to store our checked ids array.
const [checkedIds, setCheckedIds] = useState([]);
implement handler.
const handleCheck = useCallback((id) => {
return () => {
setCheckedIds(prevIds => prevIds.includes(id) ? prevIds.filter(item => item !== id) : [...prevIds, id]);
};
}, []);
render our checkboxes and apply handler.
list.map(({ id, isolatename }) => (
<FormControlLabel
key={id}
control={
<Checkbox
color="primary"
checked={checkedIds.includes(id)}
onChange={handleCheck(id)}
name={`checkbox_${id}`}
/>
}
label={isolatename}
/>)
))
ps. in case if <Checkbox/> props 'onChange' returns callback like this (isChecked: boolean) => {} we can simplify (2) step.
const handleCheck = useCallback(id => {
return isChecked => {
setCheckedIds(prevIds => isChecked ? prevIds.filter(item => item == id) : [...prevIds, id]);
};
}, []);
You may remember that it is React JS and not only JS that we are talking about.
In React you want to control data in the way of a state. There are a lot of ways to do so with check boxes, I'm contributing with one that you can see in the code snippet below:
import React, {useState} from "react";
export default function CheckBoxesControllers() {
const [checkboxes, setCheckboxes] = useState(() => [
{ id: "0", checked: false },
{ id: "1", checked: false },
{ id: "2", checked: false },
]);
const handleUpdate = (event) => {
const { target: {id, checked} } = event;
setCheckboxes(currentState => {
const notToBeUpdated = currentState.filter(input => input.id !== id);
return [
...notToBeUpdated,
{ id, checked }
]
});
}
function toggleSelectAll() {
setCheckboxes(currentState => currentState.map(checkbox => ({...checkbox, checked: !checkbox.checked})));
}
return (
<>
{checkboxes?.length ? (
checkboxes.map((checkbox, index) => {
return (
<input
checked={checkbox.checked}
id={checkbox.id}
key={index}
type="checkbox"
onChange={event => handleUpdate(event)}
/>
);
})
) : <></>}
<button onClick={toggleSelectAll}>Toggle Select All</button>
</>
)
}
This code is meant to serve you as an example of how to work properly with react state in the hook way, but there are other way, as you can see in the Documentation

How can I react to a checkbox in a map?

How can I toggle a checkbox in a map when I click on it?
So only set to true or false which one was clicked.
Then I would like to either pack the value of the checkbox, in this case the index, into an array or delete it from it.
const handleChange = (event) => {
const copy = teilen;
setChecked(event.target.checked);
if (event.target.checked)
{
setTeilen(teilen, event.target.value);
} else {
copy.splice(event.target.value, 1);
setTeilen(copy);
}
console.log(teilen);
};
[...]
pdfs.map((p, index) => (
[...]
<Checkbox
value={p._id}
index={p._id}
checked={checked}
onChange={handleChange}
inputProps={{ "aria-label": "primary checkbox" }}
/>
[...]
))}
in your case you can try something like this:
const checkboxes = [
{
name:"a",
value:"a",
checked:false
},
{
name:"b",
value:"b",
checked:false
},
{
name:"c",
value:"c",
checked:false
},
];
const checkedArray = [];
const handleChange = (e) => {
if(e.target.checked){
checkedArray.push(item.value);
checkboxes[i].checked = true;
} else {
if(checkedArray.contains(item.value)){
checkedArray.filter(arrItem => arrItem === item.value);
}
checkboxes[i].checked = false;
}
const render = () => {
return checkboxes.map((item,i) => (
<input type="checkbox"
value={item.value}
name={item.value}
checked={item.checked}
onChange={handleChange}
/>
))
}
and you must keep the data in state to make component re-render.

Resources