I am building a category adder and previously I was only allowing the "add category chip" to show for the nooptions render, but I realized that if there was a category "software ngineer" and someone wanted to add "Software" you couldn't do it.
Ultimately I want to add the Add chip (which is different then the normal chips) to the end of all of the options with the search term in it. I tried adding it to the render option, but then it showed up after every option and I tried adding it to filter options but then I can't customize that chip.
Any suggestions on solving this?
My code:
<Autocomplete
id="Categories"
// set it to the left of the cell
style={{ width: '100%', marginTop: '1rem' }}
key={autocompleteKey}
options={
user.categories.length > 0
? user.categories.filter((c) => {
return !category.includes(c || c.charAt(0).toUpperCase() + c.slice(1));
})
: []
}
onChange={(event, value) => (handleAddCategoryToCompany(value))}
onInputChange={(event, value) => setNewCategory(value)}
popupIcon=""
filterOptions={(options) => {
console.log(options);
const result = [...options];
if (search.length > 0) result.push(search);
return result;
}}
renderOption={(event, value: string) => (
<>
<Chip
variant="outlined"
sx={{
m: '2px',
}}
onClick={() => handleAddCategoryToCompany(value)}
key={Math.random()}
label={value.charAt(0).toUpperCase() + value.slice(1)}
onDelete={() => removeFromGlobal(value)}
/>
{/* {search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null} */}
</>
)}
renderInput={(par) => (
// Textfield where the options have a delete icon
<TextField
{...par}
fullWidth
label="Add Tag(s)"
margin="normal"
size="small"
name="Name"
onChange={(event) => (setSearch(event.target.value))}
key={autocompleteKey}
type="company"
value={search}
variant="outlined"
/>
)}
noOptionsText={search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null}
/>
I figured it out by adding a modifier to the search option in the filter options and then had a conditional statement in render to look for this option. Seems to be working.
<Autocomplete
id="Categories"
// set it to the left of the cell
style={{ width: '100%', marginTop: '1rem' }}
key={autocompleteKey}
options={
user.categories.length > 0
? user.categories.filter((c) => {
return !category.includes(c || c.charAt(0).toUpperCase() + c.slice(1));
})
: []
}
onChange={(event, value) => (handleAddCategoryToCompany(value))}
onInputChange={(event, value) => setNewCategory(value)}
popupIcon=""
filterOptions={(options) => {
console.log(options);
const result = [...options];
if (search.length > 0) result.push(`${search} (new)`);
return result;
}}
renderOption={(event, value: string) => (
<>
{value.indexOf(' (new)') > -1
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
)
: (
<Chip
variant="outlined"
sx={{
m: '2px',
}}
onClick={() => handleAddCategoryToCompany(value)}
key={Math.random()}
label={value.charAt(0).toUpperCase() + value.slice(1)}
onDelete={() => removeFromGlobal(value)}
/>
)}
</>
)}
renderInput={(par) => (
// Textfield where the options have a delete icon
<TextField
{...par}
fullWidth
label="Add Tag(s)"
margin="normal"
size="small"
name="Name"
onChange={(event) => (setSearch(event.target.value))}
key={autocompleteKey}
type="company"
value={search}
variant="outlined"
/>
)}
noOptionsText={search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null}
/>
Related
I am displaying cards from an array of objects with map, and I would like to show only onMouse enter some icons, but only on the card where mouse is hover. Right now, icons show on all card
const onMousseEnter = (index) => {
if (index === interestsList[index].id) {
setVisibility(true);
}
};
{interestsList
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
.map((el, index) => (
<InterestCard
key={index}
onMouseEnter={() => onMousseEnter(index)}
onMouseLeave={() => onMouseLeave(index)}>
<CategoryLeftCol onClick={openCatPage(index)}>
<SurfingRoundedIcon sx={{ fontSize: 22 }} />
<Typography variant="h6" fontWeight="bold">
{el.name}
</Typography>
</CategoryLeftCol>
{visible && (
<CategoryRightCol>
<IconButton onClick={() => setOpen(true)}>
<Edit sx={{ fontSize: 14 }} />
</IconButton>
<IconButton onClick={() => setOpenAlert(true)}>
<Delete sx={{ fontSize: 14 }} />
</IconButton>
</CategoryRightCol>
)}
I can get the index of the card where mouse is hover but problem persist...
className="interestCard" // for <InterestCard/>
...
className="interestCard_icons" // for <CategoryRightCol/> i guess
css:
.interestCard_icons {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.interestCard:hover .interestCard_icons {
opacity: 1;
pointer-events: auto;
}
try to create a new Component for using on each index of interestsList :
import { useState } from "react";
{interestsList
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
.map((el, index) => (
<ChildComponent
index={index}
el={el}
key={index}
/>
)}
function ChildComponent({el,index}){
const [visible,setVisibility] = useState(false);
const onMousseEnter = (index) => {
if (index === interestsList[index].id) {
setVisibility(true);
}
};
return (<InterestCard
key={index}
onMouseEnter={() => onMousseEnter(index)}
onMouseLeave={() => onMouseLeave(index)}>
<CategoryLeftCol onClick={openCatPage(index)}>
<SurfingRoundedIcon sx={{ fontSize: 22 }} />
<Typography variant="h6" fontWeight="bold">
{el.name}
</Typography>
</CategoryLeftCol>
{visible && (
<CategoryRightCol>
<IconButton onClick={() => setOpen(true)}>
<Edit sx={{ fontSize: 14 }} />
</IconButton>
<IconButton onClick={() => setOpenAlert(true)}>
<Delete sx={{ fontSize: 14 }} />
</IconButton>
</CategoryRightCol>)
}
I am creating my sidebar component which is divided by sections. What I want to do, is to implement drag'n'drop functionality. But here is the problem, here is how those sections look like in code:
const sidebarSections = [
{
name: 'General',
routePrefix: '/general',
elements: [
{
name: "Home",
icon: <HomeIcon/>,
href: "/home",
show: true
},
{
name: "Clients",
icon: <SupervisedUserCircleIcon/>,
href: "/clients",
show: true
},
{
name: "ACL",
icon: <ManageAccountsIcon/>,
href: "/acl",
show: true
},
{
name: "Modify roles",
icon: <AddModeratorIcon/>,
href: "/roles",
show: true
},
]
},
...
Every section - in this case General - has its elements. As you probably guess, I want to implement drag'n'drop only for those elements and not for whole sidebarSections.
Here is how this sidebar list looks like in code:
const [sidebar, setSidebar] = React.useState(sidebarSections)
const list = () => (
<Box
sx={{
width: anchor === 'top' || anchor === 'bottom' ? 'auto' : 250,
}}
role="presentation"
onKeyDown={toggleDrawer(anchor, false)}
>
<List>
{sidebarSections.map((section, key) => (
<Box
key={key}
sx={{
marginTop: 2,
}}
>
{section.name ? (
<ListItemText
sx={{
px: 2,
color: 'primary.light',
}}
>
{section.name}
</ListItemText>
) : null}
<List>
{section.elements.map((subsection) =>
subsection.show || settings ? (
<ListItem key={subsection.name}>
{settings ? (
<Checkbox
checked={subsection.show}
onChange={() => {
subsection.show = !subsection.show;
setSidebar((prev) => [...prev]);
}}
/>
) : null}
<Link href={section.routePrefix + subsection.href} passHref>
<Button
sx={{
color: router.pathname.includes(
`${section.routePrefix + subsection.href}`
)
? "secondary.main"
: "primary.main",
width: "100%",
justifyContent: "start"
}}
startIcon={subsection.icon}
>
{subsection.name}
</Button>
</Link>
</ListItem>
) : null
)}
</List>
<Divider
sx={{color: 'primary.light'}}
/>
</Box>
))}
</List>
</Box>
)
I was using this guide while was trying to do this, but, because of nested list I wasn't able to do that.
This is how I solved this problem. Instead of {subsection.name} you can pass your component (Button in my case).
DragDropContext has to be only on list where you want have drag'n'drop enabled. Also I have changed handleDrop function to make it parse nested list and render it more time to show new order of elements.
const handleDrop = (droppedItem) => {
if (!droppedItem.destination) return;
const updatedSidebar = [...sidebar];
updatedSidebar.forEach(section => {
section.elements.forEach(subsection => {
if (droppedItem.draggableId === subsection.name) {
const [reorderedItem] = section.elements.splice(droppedItem.source.index, 1);
section.elements.splice(droppedItem.destination.index, 0, reorderedItem);
}
})
})
setSidebar(updatedSidebar);
};
const list2 = () => (
<Box
sx={{
width: anchor === 'top' || anchor === 'bottom' ? 'auto' : 250,
}}
role="presentation"
onKeyDown={toggleDrawer(anchor, false)}
>
<List>
{sidebarSections.map((section, key) => (
<Box
key={key}
sx={{
marginTop: 2,
}}
>
{section.name ? (
<ListItemText
sx={{
px: 2,
color: 'primary.light',
}}
>
{section.name}
</ListItemText>
) : null}
<List>
<DragDropContext onDragEnd={handleDrop}>
<Droppable droppableId="list-container">
{(provided) => (
<div
className="list-container"
{...provided.droppableProps}
ref={provided.innerRef}
>
{section.elements.map((subsection, index) =>
subsection.show || settings ? (
<ListItem key={subsection.name}>
{settings ? (
<Box>
<Draggable key={subsection.name} draggableId={subsection.name} index={index}>
{(provided) => (
<div
className="item-container"
ref={provided.innerRef}
{...provided.dragHandleProps}
{...provided.draggableProps}
>
{subsection.name}
</div>
)}
</Draggable>
{provided.placeholder}
</Box>
) : null}
{!settings ? (
<Link href={section.routePrefix + subsection.href} passHref>
<Button
sx={{
color: router.pathname.includes(
`${section.routePrefix + subsection.href}`
)
? "secondary.main"
: "primary.main",
width: "100%",
justifyContent: "start"
}}
startIcon={subsection.icon}
>
{subsection.name}
</Button>
</Link>
) : null}
</ListItem>
) : null
)}
</div>
)}
</Droppable>
</DragDropContext>
</List>
<Divider
sx={{color: 'primary.light'}}
/>
</Box>
))}
</List>
</Box>
);
I have a custom Input Box and when I type inside a custom input component It'll re-render the typed input inside.
import {
Badge,
Box,
CloseButton,
Grid,
GridItem,
Input,
Text
} from "#chakra-ui/react";
import React, { useEffect, useState } from "react";
function InputTag(props) {
const [tags, setTags] = useState(props.values);
const removeTag = (index) => {
setTags(tags.filter((_, i) => i !== index));
};
const addTag = (event) => {
if (event.target.value !== "") {
setTags([...tags, event.target.value]);
props.setFieldValue("tags", [...tags, event.target.value]);
event.target.value = "";
}
};
useEffect(() => {
props.show === false && setTags([]);
}, [props.show]);
//update values based on click suggestions
useEffect(() => {
setTags([props.values, props.suggTag]);
}, [props.suggTag, props.values]);
return (
<Box
display={"flex"}
border="1px"
borderColor={"gray.200"}
borderRadius={"md"}
padding={2}
>
<Grid templateColumns="repeat(3, 1fr)" gap={2} overflow="visible">
{tags &&
tags.map((tag, index) => (
<GridItem key={index}>
<Badge
variant="solid"
colorScheme={"purple"}
display={"flex"}
borderRadius="full"
justifyContent="space-between"
alignItems="center"
gap={2}
>
<Text>{tag}</Text>
<CloseButton onClick={() => removeTag(index)} />
</Badge>
</GridItem>
))}
</Grid>
<Input
type="text"
name="tags"
id="tags"
variant={"unstyled"}
placeholder="Add Tag"
_placeholder={{ fontsize: "md" }}
onChange={props.handleChange}
onBlur={props.handleBlur}
onError={props.errors}
onKeyUp={(event) =>
event.key === "Enter" ? addTag(event) && event.preventDefault() : null
}
/>
</Box>
);
}
export default InputTag;
Here, when I hit enter It'll render them inside the custom Input Box
I Inserted a custom array of strings as "ex_Tag" inside Previewer.js so that when I click on the word in array, it'll also get rendered inside custom input as well.
function NewUploader({ isOpen, onClose }) {
const cancelRef = useRef();
const ex_tags = ["Design", "Strategy", "Human Centered Design"];
const [show, Setshow] = useState(true);
const [suggTag, setSuggTag] = useState();
const initialValues = {
files: null,
tags: []
};
const validationSchema = yup.object({
files: yup.mixed().required("File is Required"),
tags: yup.mixed().required("tags required")
});
const onSubmit = (values, actions) => {
const formData = new FormData();
formData.append("files", values.files[0]);
formData.append("tags", values.tags);
for (var pair of formData.entries()) {
console.log(pair[0] + ", " + pair[1]);
}
actions.setSubmitting(false);
actions.resetForm();
Setshow(!show);
onClose();
};
const handlethis = (e) => {
e.preventDefault();
};
//insert suggested word to useState so i can pass it to custom input
const handleClick = (tag) => {
setSuggTag(tag);
};
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
{/* update code on model here */}
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Text fontWeight={"bold"} color="gray.900">
Upload Buddy
</Text>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex direction="column" gap={3}>
<Box>
<Text fontWeight={"normal"} color="gray.700">
This learning contentwill not be summarised. to summarize your
content, use{" "}
<Link color={"purple.400"}>Create Knowledge Nugget</Link> option
instead.
</Text>
</Box>
<Box>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{(formik) => (
<Form
onSubmit={handlethis}
autoComplete="off"
encType="multipart/form-data"
>
<FormLabel htmlFor="file">
<Text
fontSize="sm"
fontWeight="normal"
color="gray.900"
fontFamily={"body"}
>
Upload files
</Text>
</FormLabel>
{/* drag droop sec */}
{formik.isSubmitting ? (
<>
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{formik.values.files &&
formik.values.files.map((file, index) => (
<GridItem key={index}>
<Badge
variant="solid"
borderRadius="xl"
colorScheme={"gray"}
w={file.name.length * 4}
h="8"
display="flex"
justifyContent="center"
alignItems="center"
my={2}
>
<Text fontFamily={"body"}>{file.name}</Text>
<CloseButton colorScheme={"blackAlpha"} />
</Badge>
</GridItem>
))}
</Grid>
<Progress colorScheme={"yellow"} isIndeterminate />
</>
) : (
<>
<Dragdrop setFieldValue={formik.setFieldValue} />
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{formik.values.files &&
formik.values.files.map((file, index) => (
<GridItem key={index}>
<Badge
variant="solid"
borderRadius="xl"
colorScheme={"gray"}
w={file.name.length * 4}
h="8"
display="flex"
justifyContent="space-between"
alignItems="center"
my={2}
>
<Text fontFamily={"body"}>{file.name}</Text>
<CloseButton colorScheme={"blackAlpha"} />
</Badge>
</GridItem>
))}
</Grid>
{formik.errors.files && formik.touched.files && (
<Text fontFamily={"body"} color="red">
{formik.errors.files}
</Text>
)}
</>
)}
<FormErrorMessage>
<ErrorMessage name="file" />
</FormErrorMessage>
<FormLabel htmlFor="tags">
<Text
fontSize="sm"
fontWeight="normal"
color="gray.900"
fontFamily={"body"}
>
Tags
</Text>
</FormLabel>
<InputTag
setFieldValue={formik.setFieldValue}
handleChange={formik.handleChange}
handleBlur={formik.handleBlur.call}
values={formik.values.tags}
show={show}
suggTag={suggTag}
/>
{formik.errors.tags && formik.touched.tags && (
<Text fontFamily={"body"} color="red">
{formik.errors.tags}
</Text>
)}
<FormErrorMessage>
<ErrorMessage name="tags" />
</FormErrorMessage>
<Box
aria-invalid="true"
display={"flex"}
flexDir="row"
gap={2}
my={2}
>
<Text fontFamily={"body"}>Suggested</Text>
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{ex_tags.map(
(tag, index) => (
<GridItem key={index}>
//I inserted on click call here
<Box onClick={handleClick(tag)}>
<Badge
variant={"subtle"}
borderRadius="lg"
colorScheme={"gray"}
_hover={{
cursor: "pointer",
bgColor: "gray.200"
}}
>
<Text fontFamily={"body"}>{tag}</Text>
</Badge>
</Box>
</GridItem>
),
this
)}
</Grid>
</Box>
<Box display={"flex"} justifyContent="center" my={3}>
<Button
type="button"
ref={cancelRef}
colorScheme="yellow"
isLoading={formik.isSubmitting}
onClick={formik.handleSubmit}
>
<Text
fontWeight="bold"
fontSize="18px"
color="gray.900"
fontFamily={"body"}
>
Submit
</Text>
</Button>
</Box>
</Form>
)}
</Formik>
</Box>
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}
export default NewUploader;
but It seems when I render them to the screen it will come out as I triggered the onClick even though I didn't.
For now I commented out the useEffect func inside input component
I have uploaded it to code sandbox Bellow.
https://codesandbox.io/s/amazing-heyrovsky-9kr0ex?file=/src/Previewer.js
I have two autocomplete components in my form where one depends on the other. I want that at a choice of a field in the first component values in the second were reset. I tried a bunch of ways to do it, but nothing worked. Please, tell me how can I solve this problem?
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmitAppealForm)}>
<Grid item container xs={12} sx={{ mt: 2 }} columnSpacing={3}>
<Grid item xs={6}>
<SelectControl
required
control={control}
options={[
{ id: 1, name: "1 - Тематика" },
{ id: 2, name: "2 - Категория" },
{ id: 3, name: "3 - Подкатегория" },
]}
placeholder="Уровень тематики"
label="Уровень тематики *"
name="themeLevel"
readonly
/>
</Grid>
<Grid item xs={6}>
<SelectControl
control={control}
options={getProjectThemesArray() ?? []}
disabled={!themeLevel || themeLevel === 1}
label="Родительская тематика"
defaultValue=""
name="parentThemeId"
loading={isParentThemeFetching}
/>
</Grid>
</Grid>
</form>
</FormProvider>
Autocomplete component:
return (
<>
<Controller
name={name}
control={control}
render={({ field: { value, onChange: handleChange } }) => (
<>
<div className="form-label">{label}</div>
<Autocomplete
onChange={(e, value) => {
handleChange(e, value);
onChange && onChange();
}}
options={options}
noOptionsText="Нет данных"
clearText=""
closeText=""
openText=""
autoHighlight
disabled={loading || disabled}
renderInput={(params) => (
<OutlinedInput
name={name}
{...params.InputProps}
inputProps={{
...params.inputProps,
}}
value={options.length > 1 ? value?.name : ""}
readOnly={readonly}
className={input}
required={required}
placeholder={
defaultValue && disabled
? defaultValue
: placeholder ?? label
}
fullWidth
/>
)}
isOptionEqualToValue={(option, value) => option.id === value.id}
getOptionLabel={(option) => option.name || ""}
renderOption={(props, option) => (
<MenuItem {...props} key={option.id} value={option.id}>
{option.name}
</MenuItem>
)}
popupIcon={
<div className={icon}>
{loading ? <Spinner width={20} height={20} /> : <Arrow />}
</div>
}
/>
</>
)}
/>
</>
I used reset and setField from useForm to my component but it didn't work. Also, resetting the state of the input did not work either.
I like the Chip option when multiple is true. Is there an option to enable Chip also when multiple is false ?
<Autocomplete
className={classes.search}
options={top100Films}
getOptionLabel={(option) => option.title}
multiple={false}
renderInput={(params) =>
<TextField {...params}
variant="outlined"
label="Customer's journey"
helperText="Search by: program"
/>}
/>
Thank you
No, unfortunately you would have to change the Autocomplete to a controlled component and do this yourself via the startAdornment prop on the Input. Something like:
function MyAutocomplete() {
const [value, setValue] = useState(null);
const chip = value ?
<Chip label={value.title} onDelete={() => setValue(null)} size="medium" />
: undefined;
return (
<Autocomplete
size="medium"
options={top100Films}
getOptionLabel={(option) => option.title}
onChange={(event, newValue) => setValue(newValue)}
value={value}
renderInput={(params) => (
<TextField
{...params}
InputProps={{
...params.InputProps,
startAdornment: chip
}}
variant="outlined"
label="Customer's journey"
helperText="Search by: program"
/>)}
/>);
}
Demo here