Disabled options in react dropdown depending on specific conditions - reactjs

I am currently building a dropdown component in react. It already works fine - I now would like to have some options disabled only when a specific condition occurs. In this case I have two dropdowns which work as a filter system. The first is a "from"-filter and the second a "to"-filter. If you select an option in the "from"-dropdown, I'd like the "to"-dropdown to disable the selected and all previous options.
This is my code:
import {
Dropdown,
IDropdownStyles,
IDropdownOption,
} from "#fluentui/react";
interface DropdownProps {
options: IDropdownOption[],
placeholder: string,
setDropdown: Dispatch<SetStateAction<string>>
}
const dropwdownStyles: Partial<IDropdownStyles> = {
dropdown: { width: 300 },
};
export const DropdownId = (props: DropdownProps) => {
return (
<div className={styles.root}>
<Dropdown
placeholder={props.placeholder}
options={props.options}
styles={dropwdownStyles}
onChange={(e, options) => {
props.setDropdown(`${options?.key}`)
}}
/>
</div>
);
};
Implementing the dropdown in index.tsx:
<Container>
<DropdownId options={apiTagsResponse.map(tag => ({ text: tag, key: tag }))} setDropdown={setStartObjectId} placeholder="Set Start Tag" />
</Container>
<Container>to</Container>
<Container>
<DropdownId options={apiTagsResponse.map(tag => ({ text: tag, key: tag }))} setDropdown={setEndObjectId} placeholder="Set End Tag" />
</Container>
<Container>
<DropdownApplyFilterButton onClick={() => setButtonClicked(true)} />
</Container>
The result:

Related

Storing State in React JS not working as excepted

what i am creating here is a sorter with 3 inputs like shown here . This sorter will get some data from a table. Now i'm setting state with initSelect and i'm passing it the fields array but when i console.log(select) it gives me the object shown in the image which is incorrect from the behaviours i want {sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
The first input has to have a default value of 'Date' always , but it can change to other values in the dropdown list like name , username ect . When i console log the select state it is messed up as it's always selecting the last one on the fields array , how can i change the initSelects function to correctly build the object i want.
Also the tricky thing which i can't seem to do is , if this Date value is selcted , in the second input, the date value should not be there. And if in the second input we select another value like Earth , Earth and Date should not be in the 3rd input and so on. So basically it means filtering out values . I need serious help as this is for the company i work on
Excepted Behaviour: Dynamically update value every time i select one input element like
{sorterParam1: 'Date', sorterParam2: '', sorterParam3: ''}
When selectin 'Date' for example , it shouldn't not be included in the dropdownlist on sorterParam2, sorterParam3.
/*eslint-disable*/
import React, { useState, useMemo } from 'react';
import TextField from '#mui/material/TextField';
import Button from '#mui/material/Button';
import { GridSortModel } from '#mui/x-data-grid';
import SorterField from './SorterField';
const initSelects = (fields) => {
let object = {};
fields.map((item) => {
console.log(item, 'item');
object = {
...item,
[item.name]: item.defaultValue ? item.defaultValue : '',
};
});
return object;
};
const Sorter = ({ menuItemsValue, setSortData }: SortProps) => {
const fields: SorterProps[] = [
{
name: 'sorterParam1',
title: 'Sort by',
optional: false,
defaultValue: 'Date',
},
{
name: 'sorterParam2',
title: 'Then by',
optional: true,
},
{
name: 'sorterParam3',
title: 'Then by',
optional: true,
},
];
const [select, setSelect] = useState<any>(() => initSelects(fields));
const getMenuItems = useMemo(() => {
return menuItemsValue.filter((item) => select.sorterParam1 !== item);
}, [menuItemsValue, select]);
const handleSelectChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
console.log(111, name, value);
setSelect({ ...select, [name]: value });
setSortData(sortOptions);
};
const handleClearAllInputs = () => {
setSelect({
sorterParam1: '',
sorterParam2: '',
sorterParam3: '',
});
};
const handleConfirm = () => {};
return (
<TextField
label="Advanced Sorting"
className={styles.sorter__inputs}
id="sorter-parameter-1"
variant="standard"
InputProps={{
disableUnderline: true,
}}
select
SelectProps={{
IconComponent: (props) => <NewIcon {...props} />,
}}
sx={{
fontSize: '12px',
width: '100%',
'& .MuiInputBase-input:focus': {
backgroundColor: 'transparent !important',
},
'& .MuiInputLabel-root': {
color: '#9E9E9E',
},
'& .MuiTextField-root': {
fontSize: '13px',
},
'& .MuiOutlinedInput-root': {
backgroundColor: '#fff',
},
}}
>
{fields.map((option, index) => (
<SorterField
key={option.name}
menuItemsValue={getMenuItems}
name={option.name}
option={option}
count={fields.length}
handleChange={handleSelectChange}
index={index + 1} // setData={setData}
/>
))}
<div className={styles.sorter__inputControllers}>
<Button
className={styles.sorter__clearAllInput}
onClick={() => handleClearAllInputs()}
>
Clear All
</Button>
<Button
onClick={() => handleConfirm()}
className={styles.sorter__confirmInput}
>
Confirm
</Button>
</div>
</TextField>
);
};
export default Sorter;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
This is the SorterField Component code if that might be helpful
/*eslint-disable*/
import React, { useState } from 'react';
import TextField from '#mui/material/TextField';
import { MenuItem } from '#mui/material';
import { SorterProps } from '../../types/Sorter';
import { ReactComponent as SorterLine } from '../../assets/img/sortLine.svg';
import styles from '../../assets/components/Sorter/sorter.module.scss';
type SorterFieldProps = {
menuItemsValue: string[];
option: SorterProps;
count: number;
name: string;
handleChange: (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
index: number;
};
function SorterField({
option,
count,
menuItemsValue,
handleChange,
index,
}: SorterFieldProps) {
const handleSorting = () => {};
return (
<div className={styles.sorter__container}>
<div className={styles.sorter__header}>
<p className={styles.sorter__label}>
{option.title}{' '}
{option.optional && (
<sup className={styles.sorter__optional}>*Optional</sup>
)}
</p>
<div className={styles.sorter__numbers__container}>
{Array.from({ length: count }, (_, i) => i + 1).map((number) => (
<>
{number === index ? (
<>
<span className={styles.sorter__number}>{index}</span>
</>
) : (
<>
<span className={styles.sorter__numbers}>{number}</span>
</>
)}
</>
))}
</div>
</div>
<div className={styles.sorter__inputs}>
<TextField
className={[styles.sorter__input, styles.sorter__field__input].join(
' '
)}
variant="outlined"
label="Select"
select
SelectProps={{
IconComponent: () => <NewIcon />,
}}
value={option.defaultValue}
onChange={handleChange}
name={option.title}
size="small"
>
{menuItemsValue.map((title, idx) => (
<MenuItem key={idx} value={title}>
{title}
</MenuItem>
))}
</TextField>
<div onClick={handleSorting} className={styles.sorter__sortOrder}>
<div className={styles.sorter__sortOrder__alphabetical}>
<span>A</span>
<span>Z</span>
</div>
<div className={styles.sorter__sortOrder__sortLine}>
<SorterLine />
</div>
</div>
</div>
</div>
);
}
export default SorterField;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You're re-assigning a value to object in each iteration of fields.map().
If you want to build object as a map of field item name to defaultValue (or blank string), use this instead...
return Object.fromEntries(
fields.map(({ name, defaultValue }) => [name, defaultValue ?? ""])
);
See Object.fromEntries()
Also, move the fields declaration outside your component. It's static content so can be omitted from being declared every render and from any hook dependencies.
You could also use fields.reduce() which is basically the same thing
return fields.reduce(
(obj, { name, defaultValue }) => ({
...obj,
[name]: defaultValue ?? "",
}),
{}
);
As for removing selected options as you iterate through the fields, that's a little trickier.
You could use a memo hook to create an iterable data structure that includes the available options for that particular iteration.
For example
const fieldsWithOptions = useMemo(() => {
const taken = new Set(Object.values(select));
const available = menuItemsValue.filter((item) => !taken.has(item));
return fields.map((field) => ({
...field,
options: select[field.name]
? [select[field.name], ...available] // include the current selection
: available,
}));
}, [menuItemsValue, select]);
Then map over fieldsWithOptions instead of fields and use option.options instead of getMenuItems.

Object array can't add items on React chips from material-ui

I'm with a little issue here right now with Material-UI chips component. I have a select with a list of users. When you select a user, a chip with his number appears right below the form. Everything works just fine but the chips are overwriting, not adding the new ones. Here my code:
import React from 'react';
import Chip from '#material-ui/core/Chip';
export default function Test(){
const [chipData, setChipData] = React.useState([]);
const userArray = [
{key: 1, label: name1},
{kel: 2, label: name2}
];
const [userValue, setUserValue] = React.useState('');
// This come from the select form onChange
const handleSelect = (e) => {
setUserValue(e.target.value);
setChipData([...chipData, {key: e.target.value, label: e.target.value}]);
}
const handleDelete = chipToDelete => () => {
setChipData(chips => chips.filter(chips => chips.key !== chipToDelete.key));
}
return(
<div>
<TextField
select
value={userValue}
onChange={e => handleSelect(e)}
>
{userArray.map(option => (
<MenuItem key={option.key} value={option.label}>
{option.label}
</MenuItem>
))}
</TextField>
{chipData.map(data => {
return(
<Chip
key={data.key}
label={data.label}
onDelete={handleDelete(data)}
/>
);
})}
</div>
);
}
I tried with something like chipData.push({data...}) that works just fine, but do not allow me to add more items if I delete any of them. With the code above the chips are just overwriting the one clicked before and do not add to the array.
Online Example, please validate if you have the issue:
https://codesandbox.io/s/material-ui-n7kbp
only edited key render values:
{chipData.map((data, index) => {
return (
<Chip
key={data.key + index}
label={data.label}
onDelete={handleDelete(data)}
/>
);
})}

Formik Validation in a Field Array

I have a formik form like this:-
import Drawer from "components/atoms/Drawer";
/* import Input from "components/atoms/Input";
import InputGroup from "components/atoms/InputGroup";
import Label from "components/atoms/Label"; */
import Scrim from "components/atoms/Scrim";
import DrawerBody from "components/molecules/DrawerBody";
import { Field, FieldArray, Form, FormikErrors, FormikProps, withFormik } from "formik";
import { ITrackedPage } from "hocs/withAppDynamics";
import * as React from "react";
import { Box, Flex, Text } from "rebass";
import * as AmenitiesActions from "store/amenities/actions";
import { IAmenity, IAmenityRanking } from "store/amenities/models";
import DrawerHeader from "./DrawerHeader";
// import ErrorMessage from 'components/atoms/ErrorMessage';
interface IAmenitiesDrawerProps {
drawerOpen: boolean;
onDrawerClose: () => void;
tenantAssessmentId: string;
actions: typeof AmenitiesActions;
maxRank?: number;
}
interface IAmenitiesDrawerValues {
amenitieslist: IAmenity[];
}
const InnerForm: React.FC<
IAmenitiesDrawerProps & ITrackedPage & FormikProps<IAmenitiesDrawerValues>
> = ({
errors,
drawerOpen,
onDrawerClose,
handleChange,
values,
setValues,
isValid,
tenantAssessmentId,
sendAnalyticsData,
actions
}) => {
const handleDrawerClose = () => {
onDrawerClose();
};
return (
<>
<Scrim isOpen={drawerOpen} onClose={handleDrawerClose} />
<Drawer isOpen={drawerOpen} direction="right" drawerWidth="700px">
<DrawerHeader handleCloseDrawer={handleDrawerClose} />
<DrawerBody p={5}>
<Flex mb={4}>
<Box flex={1}>
<Text fontWeight="light" fontSize={4} mt={3} mb={4}>
Add custom amenities
</Text>
</Box>
</Flex>
<Form>
<FieldArray
name="amenitieslist"
render={arrayHelpers => (
<div>
{// values.amenitieslist && values.amenitieslist.length > 0 ? (
values.amenitieslist.map((amenity, index) => (
<div key={index}>
<Field name={`amenitieslist.${index}.name`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
>
-
</button>
{errors.amenitieslist}
</div>
))}
<button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
{/* show this when user has removed all amenities from the list */}
Add a Amenity
</button>
</div>
)}
/>
<div>
<button type="submit">Submit</button>
</div>
</Form>
</DrawerBody>
</Drawer>
</>
);
};
export const AmenitiesDrawer = withFormik<IAmenitiesDrawerProps, IAmenitiesDrawerValues>({
enableReinitialize: true,
handleSubmit: (values, {props}) => {
const seed: number = props.maxRank? props.maxRank : 0;
const amenityRankings: IAmenityRanking[] = values.amenitieslist.map((a, index)=>({
amenityId: 1,
rank: index + 1 + seed,
amenityName: a.name,
customAmenity: true
}));
console.log(amenityRankings);
console.log(props.actions.customSaveAmenities);
console.log(props.tenantAssessmentId);
props.actions.customSaveAmenities(props.tenantAssessmentId, amenityRankings);
},
mapPropsToValues: ({tenantAssessmentId}) => ({
amenitieslist:[{id: 0, name: '', imageUrl: '', description: '', tenantAssessmentId}]
}),
validate: values => {
const errors: FormikErrors<{ validAmenity: string }> = {};
console.log('In the Validate method');
const { amenitieslist } = values;
const amenityValid = amenitieslist[0].name.length < 28;
if (!amenityValid) {
console.log('Amenity is not valid');
errors.validAmenity = "Amenity needs to be atmost 28 characters";
console.log(errors);
}
return errors;
}
})(InnerForm);
As you all can see I have a text input. I want to throw a error message below the text field when the length is more than 28 characters long.
How is this possible ? Please help me with this.
I find the most convenient way to validate Formik forms is using yup as recommended in their documentation. You can define a validation schema and pass it as a prop to the main Formik component (or HOC as it appears you're using) and remove your custom validation function:
validationSchema: yup.object().shape({
amenitieslist: yup.array()
.of(yup.object().shape({
name: yup.string().max(28, "Max 28 chars")
// Rest of your amenities object properties
}))
})
And then in your FieldArray:
<FieldArray
name="amenitieslist"
render={arrayHelpers => (
<div>
{ values.amenitieslist && values.amenitieslist.length > 0 ? (
values.amenitieslist.map((amenity, index) => (
<div key={index}>
<Field name={`amenitieslist[${index}].name`} />
<button
type="button"
onClick={() => arrayHelpers.remove(index)} // remove a amenity from the list
>
-
</button>
{errors.amenitieslist[index].name}
</div>
))}
<button type="button" onClick={() => arrayHelpers.push({ id: "", name: "", imageUrl: '', description: '', tenantAssessmentId })}>
{/* show this when user has removed all amenities from the list */}
Add a Amenity
</button>
</div>
)}
/>
I just changed the accessors you were using to designate the field name (to use an index for an array element, you have to use bracket notation) and where to find the errors, yup should generate them automatically. Tough to know for sure I'm not missing anything without testing it, hope this helps!
I wasn't using yup so the other solutions weren't useful for me. What I did was is have another value that can represent your error. I loop through my fieldarray and assign an error.
values.payments.forEach((element) => {
if (Number(element.isBTC) === 1 && !values.btcCompanyId) {
errors.btcCompany =
"For this payment account, BTC Company must be selected";
}
});
You can make something like this for yourself. I sometimes have an _action value given to the last button and have errors show there. You'll figure it out. Not the right solution if you want the error to display for each fieldarray.

How to use my react component PlaceInput to achieve place autocomplete in the menu input box?

I have a PlaceInput component which support google place autocomplete.
class PlaceInput extends Component {
state = {
scriptLoaded: false
};
handleScriptLoaded = () => this.setState({ scriptLoaded: true });
render() {
const {
input,
width,
onSelect,
placeholder,
options,
meta: { touched, error }
} = this.props;
return (
<Form.Field error={touched && !!error} width={width}>
<Script
url="https://maps.googleapis.com/maps/api/js?key={my google api key}&libraries=places"
onLoad={this.handleScriptLoaded}
/>
{this.state.scriptLoaded &&
<PlacesAutocomplete
inputProps={{...input, placeholder}}
options={options}
onSelect={onSelect}
styles={styles}
/>}
{touched && error && <Label basic color='red'>{error}</Label>}
</Form.Field>
);
}
}
export default PlaceInput;
I also have a menu item which is an<Input> from semantic-ui-react. The frontend is like below:
The menu code is like below:
<Menu.Item>
<Input
icon='marker'
iconPosition='left'
action={{
icon: "search",
onClick: () => this.handleClick()}}
placeholder='My City'
/>
</Menu.Item>
How can I leverage the PlaceInput component to make menu <Input> box to achieve the place autocomplete? Thanks!
If you could share a working sample of your app (in e.g. codesandbox) I should be able to help you make your PlaceInput class work with the Menu.Input from semantic-ui-react.
Otherwise, you can test a fully working example of such integration with the code below, which is based off of the Getting Started code from react-places-autocomplete.
import React from "react";
import ReactDOM from "react-dom";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng
} from "react-places-autocomplete";
import { Input, Menu } from "semantic-ui-react";
const apiScript = document.createElement("script");
apiScript.src =
"https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places";
document.head.appendChild(apiScript);
const styleLink = document.createElement("link");
styleLink.rel = "stylesheet";
styleLink.href =
"https://cdn.jsdelivr.net/npm/semantic-ui/dist/semantic.min.css";
document.head.appendChild(styleLink);
class LocationSearchInput extends React.Component {
constructor(props) {
super(props);
this.state = { address: "" };
}
handleChange = address => {
this.setState({ address });
};
handleSelect = address => {
geocodeByAddress(address)
.then(results => getLatLng(results[0]))
.then(latLng => console.log("Success", latLng))
.catch(error => console.error("Error", error));
};
render() {
return (
<PlacesAutocomplete
value={this.state.address}
onChange={this.handleChange}
onSelect={this.handleSelect}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<Menu>
<Menu.Item>
<Input
icon="marker"
iconPosition="left"
placeholder="My City"
{...getInputProps({
placeholder: "Search Places ...",
className: "location-search-input"
})}
/>
</Menu.Item>
</Menu>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map(suggestion => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "#fafafa", cursor: "pointer" }
: { backgroundColor: "#ffffff", cursor: "pointer" };
return (
<div
{...getSuggestionItemProps(suggestion, {
className,
style
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
);
}
}
ReactDOM.render(<LocationSearchInput />, document.getElementById("root"));
Hope this helps!

Need TextField to update text after state is changed

I need to make a small change to a textfield. Basically there is a reset button that clears the search. The user can write the search string in the TextField. But the clear function only resets the actual search, but not the TextField which then still contains some kind of search string the user have written.
interface DrawerContainerProps {
dataProfile: DataProfile;
}
interface DrawerContainerComponentProps extends DrawerContainerProps, WithStibStyles<typeof styles>, WithNamespaces {
}
interface DrawerCantainerState {
attributeSearchText: string;
}
class ProfileDrawerContainerComponent extends React.Component<DrawerContainerComponentProps, DrawerCantainerState> {
readonly state: Readonly<DrawerCantainerState> = {
attributeSearchText: ""
};
setAttributeSearchText = debounce((searchText) => this.setState({ attributeSearchText: searchText }), 200);
onSearch = (event: React.ChangeEvent<HTMLInputElement>) => this.setAttributeSearchText(event.target.value);
clearSearch = () => this.setAttributeSearchText("");
render() {
const { classes, dataProfile, t } = this.props;
return (
<Drawer
variant="permanent"
classes={{ paper: classes.drawerPaper }}>
<div className={classes.toolbar} />
<div className={classes.menu}>
<Typography className={classes.drawerTitle}>{t("drawer.objectType", { defaultValue: "Object Type Filter" })}</Typography>
<div className={classes.objectTypes}>
{dataProfile && <ObjectTypeDrawer
objectTypes={dataProfile.profiledObjectTypes}
objectCount={dataProfile.objectCount}
businessConditionFiltering={dataProfile.businessConditionFilteringResult} />}
</div>
<div className={classes.attributeTitleSearch}>
<Typography className={classes.drawerTitle}>{t("drawer.attributes", { defaultValue: "Attributes" })}</Typography>
<TextField
id="attribute-search"
placeholder={t("drawer.search", { defaultValue: "Search" })}
type="search"
className={classes.searchField}
onChange={this.onSearch }
/>
</div>
<div className={classes.attributes}>
<AttributesDrawer attributeFilter={this.state.attributeSearchText} sendFunction={this.clearSearch} />
</div>
</div>
</Drawer>
);
}
}
My knowledge of web programming is very elementary. But I suspect that whenever the clearSearch function is called, it also has to update the value of the TextField. But that is where my knowledge of React and state handling comes short. Hope someone can give a hint.
You need to 'control' the value of your TextField. That is
<TextField
id="attribute-search"
placeholder={t("drawer.search", { defaultValue: "Search" })}
type="search"
className={classes.searchField}
onChange={this.onSearch }
value={this.state.attributeSearchText}
/>

Resources