I have the following functions which display either a TextField or a Select component based on a JSON value being either Text or Select. When I try to type into a TextField, I can only enter one letter before focus is lost on the TextField. I can then click the TextField again and enter another letter before focus is lost again. There is also a noticeable delay between pressing a key and the corresponding symbol appearing in the TextField.
The complete code can be found here
This is my code of how I get my API response, useEffect, and my TextDetails function which displays the TextFields
const fetchDetails = async () => {
setBusy(true);
setDetails(await fetch(`/fiscalyears/FY2023/intakes/${params.id}/details`).then((response) => response.json()));
setBusy(false);
};
useEffect(() => {
fetchDetails();
}, []);
const TextDetails = ({ head, val, i }) => {
return (
<TextField
value={val || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[i] = {
...update[i],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
variant="outlined"
margin="normal"
label={head}
/>
);
};
const detailsComps = details["fields"]?.map((row, i) => {
return row["FieldType"] === "Text" ||
row["FieldType"] === "Decimal" ||
row["FieldType"] === "Number" ||
row["FieldType"] === "Date" ? (
<TextDetails head={row?.FieldName} val={row?.Value} i={i} />
) : (
<SelectDetails head={row.FieldName} val={row?.Value} choices={row?.Choices} i={i} />
);
});
...
return (
<Box>
{detailsComps}
</Box>
)
Sorry can't see the whole code. But please check whether the whole From is re-rendered every time you enter a value in the field. The reason could be that with setDetails you signalise React that details value has changed and because detailsComps renders from details it is highly likely the from is re-rendered. Hence, focused is lost.
I fixed this issue by moving this functionality into the functions return statement:
...
return (
<Box>
{details["fields"]?.map((row, index) => {
if (row?.FieldType === "Text" || row?.FieldType === "Decimal" || row?.FieldType === "Number") {
return (
<TextField
className="text-field"
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
label={row["FieldName"]}
/>
);
}
if (row?.FieldType === "Date") {
return (
<TextField
type="date"
label={row["FieldName"]}
InputLabelProps={{
shrink: true,
}}
/>
);
} else {
return (
<TextField
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
select
label={row?.FieldName}
>
{row?.Choices.map((choice) => (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
))}
</TextField>
);
}
})}
</Box>
)
Related
I have a Formik form that is using a progressive stepper and have multiple fields across different components, thus requiring the need to store the values in React Context. However none of the field values are being passed, so when I click submit, all values are empty strings and the validation fails. You can see on each Formik Field i am setting the value as {state.[field]}, which comes from the Context, so I believe something is going wrong here. Can anyone see what I'm doing wrong?
Thanks a lot
Here is my parent component
const AddSongPage = () => {
const { state, dispatch } = useUploadFormContext();
const initialValues = {
name: "",
};
const { mutate: handleCreateTrack } = useCreateSyncTrackMutation(
gqlClient,
{}
);
const handleSubmit = (values: any) => {
handleCreateTrack(
{
...values,
},
{
onSuccess() {
console.log("Track added succesfully");
},
}
);
};
const validate = Yup.object({
name: Yup.string().required("Song name is required"),
description: Yup.string().optional(),
});
return (
<Layout headerBg="brand.blue">
<Formik
onSubmit={(values) => handleSubmit(values)}
initialValues={initialValues}
validationSchema={validate}
>
<Form>
<Box> {state.activeStep === 1 && <Step1 />}</Box>
<Box> {state.activeStep === 2 && <Step2 />}</Box>
<Box> {state.activeStep === 3 && <Step3 />}</Box>
<Button type={"submit"}>Submit</Button>
</Form>
</Formik>
</Layout>
);
};
Here is step 1
const Step1 = () => {
const { state, dispatch } = useUploadFormContext();
const onInputChange = (e: FormEvent<HTMLInputElement>) => {
const inputName = e.currentTarget.name;
dispatch({
type: "SET_UPLOAD_FORM",
payload: {
[inputName]: e.currentTarget.value,
},
});
};
return (
<Stack spacing={4}>
<Field name={"name"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.name}
/>
</FormControl>
)}
</Field>
<Field name={"description"}>
{({ field, form }: any) => (
<FormControl isInvalid={form.errors.name && form.touched.name}>
<Input
{...field}
onChange={onInputChange}
value={state.description}
/>
</FormControl>
)}
</Field>
</Stack>
);
};
export default Step1;
I noticed that "onAdd" property is removed from the updated version of Material-UI, MUI. The only property that is a function is "onDelete" and "onClick". I want to create new chips depending on user input tags. Is there any equivalent way of doing so?
You can use input fields to trigger the creation of new chips. This jsFiddle demo (click "Run" to start the demo) has a button that creates chips.
The important code in that demo relating to your question is below under index.jsx. The key items are:
createNewChip function,
chipName TextField, and
Create New Chip button.
The createNewChip function acts as an event listener for the onClick event of the Create New Chip button. createNewChip takes the text value from the chipName TextField and adds it to the list variable which is managed by React.useState.
list is an array of chip data where each element is an object that looks something like this:
{
id: '11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000',
value: 'A Wonderful Chip',
isValid: true
}
Hopefully, this helps to get you started on a solution.
index.jsx
...
export default function ChipMaker() {
const [ list, setList ] = React.useState( [] );
const selectedItems = list.filter( ( item ) => item.isValid );
const selectedLengthIndex = selectedItems.length - 1;
let listById = {};
for ( let item of list ) {
listById[ item.id ] = item;
}
...
function createNewChip() {
let chipName = document.getElementById( 'chipName' ).value;
let newList = list.concat( {
id: uuidv4(),
value: chipName,
isValid: true
} );
setList( newList );
}
...
return (
<Stack spacing={3} sx={{ width: 500 }}>
<Autocomplete
multiple
id="tags-filled"
filterSelectedOptions={ true }
options={ list.map(( item ) => item.id) }
value={ list.map(( item ) => item.id) }
freeSolo
renderTags={( listIds, getTagProps) =>
listIds.map(( id, index) => (
<Chip
key={ index }
variant="outlined"
label={ listById[ id ].value }
sx={ {
color: ( theme ) => {
let chipColor = '#fff';
if ( typeof( listById[ id ] ) == 'object' ) {
chipColor = listById[ id ].isValid
? theme.palette.common.white
: theme.palette.common.white
}
return chipColor;
},
backgroundColor: ( theme ) => {
let chipColor = '#fff';
if ( typeof( listById[ id ] ) == 'object' ) {
chipColor = listById[ id ].isValid
? theme.palette.primary.main
: theme.palette.error.main
}
return chipColor;
},
[`& .MuiSvgIcon-root.MuiSvgIcon-fontSizeMedium.MuiChip-deleteIcon.MuiChip-deleteIconMedium.MuiChip-deleteIconColorDefault.MuiChip-deleteIconOutlinedColorDefault`]: {
fill: ( theme ) => theme.palette.grey[200]
}
} }
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="Material-UI Chip Input Test"
placeholder="Favorites"
/>
)}
onChange={ validateInput }
/>
<div>
{ selectedItems.map( ( item, index ) => {
let comma = null;
if ( selectedLengthIndex != index ) {
comma = (<span key={ 'idx'+index }>, </span>);
}
return (
item.isValid
? <span key={ index }>{ item.value }{ comma }</span>
: null
);
} ) }
</div>
<TextField
id='chipName'
name='chipName'
className={ 'test' }
type='text'
label={ 'Chip name' }
fullWidth={ false }
variant='standard'
inputProps={ { maxLength: 20 } }
helperText={ 'Enter chip name' }
InputLabelProps={ {
'variant': 'standard',
'color': 'primary',
'disableAnimation': false
} }
FormHelperTextProps={ { 'variant': 'standard' } }
error={ false }
defaultValue={ '' }
placeholder={ 'New chip name' }
color={ 'info' }
/>
<Button
variant={ 'contained' }
onClick={ createNewChip }
sx={ {
width: '200px'
} }
>
{ 'Create new chip' }
</Button>
</Stack>
);
}
/**
* Inject component into DOM
*/
root.render(
<ChipMaker />
);
Actually, I've just come to find out myself that MUI has a component called, "Autocomplete" which is the equivalent of making new chips (and adding tags from user input) with the "ChipInput" component from the older material-UI.
I'm currently trying to create a dynamic select/input component where you can choose values from select options or
type your own value inside an input field by selecting the "other" select option.
Right now I get stuck by updating the form data equally to the value of the selected option / input value. The Form Data Value always persist on the initial / default value.
App.js
...
export default function App() {
const methods = useForm({});
const { handleSubmit } = methods;
const customSalutationOptions = [
{ title: "Not specified", value: "null" },
{ title: "Male", value: "male" },
{ title: "Female", value: "female" }
];
const defaultValues = {
salutation: "null"
};
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<SelectOrInput
variant="outlined"
name={`contactPerson[0].salutation`}
defaultValue={defaultValues}
selectOptions={customSalutationOptions}
/>
<Button type="submit" color="primary" fullWidth variant="contained">
Submit
</Button>
</form>
</FormProvider>
</div>
);
}
components/SelectOrInput.tsx
...
type Props = {
name: string;
label: string;
selectOptions: [{ title: string; value: string }];
defaultValue: any;
shouldUnregister: boolean;
variant: "filled" | "outlined" | "standard";
};
export default function SelectOrInput({
name,
label,
selectOptions,
defaultValue,
shouldUnregister,
variant
}: Props) {
const classes = useStyles();
const { control } = useFormContext();
const [showCustomInput, setShowCustomInput] = useState(false);
const [value, setValue] = useState(selectOptions[0].value);
const additionalInput = [{ title: "Other", value: "" }];
const combindedOptions = selectOptions.concat(additionalInput);
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
console.log(value);
setValue(value);
} else {
setValue(value);
}
};
const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
const newState = !showCustomInput;
setValue(combindedOptions[0].value);
setShowCustomInput(newState);
};
return (
<>
{showCustomInput ? (
<FormControl className={classes.input}>
<Controller
name={name}
control={control}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<TextField
{...field}
label={label}
InputLabelProps={{ shrink: true }}
variant={variant}
placeholder="Other..."
autoFocus
type="text"
onChange={handleInputSelectChange}
value={value}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
size="small"
onClick={resetCustomInputToSelect}
id="custominput-closebutton"
>
<CloseIcon fontSize="small" />
</IconButton>
</InputAdornment>
)
}}
></TextField>
)}
/>
</FormControl>
) : (
<FormControl className={classes.input} variant={variant}>
<InputLabel id={`label-select-${label}`}>{label}</InputLabel>
<Controller
name={name}
defaultValue={defaultValue}
control={control}
shouldUnregister={shouldUnregister}
render={({ field }) => (
<Select
{...field}
label={label}
labelId={`label-select-${label}`}
value={value}
MenuProps={{
anchorOrigin: {
vertical: "bottom",
horizontal: "left"
},
getContentAnchorEl: null
}}
onChange={handleInputSelectChange}
>
{combindedOptions.map((option, index) => (
<MenuItem key={option.title} value={`${option.value}`}>
{option.title}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
)}
</>
);
}
...
To give a better example I provided a CSB:
You are storing value in it's own state of SelectOrInput component. You need to lift state up to parent component in order to get value in parent.
Create state in parent component and initialize with default value and create function to change it's value
const [inputValue, setInputValue] = useState(null);
const onChange = (value) => {
setInputValue(value);
};
Pass onChange function in SelectOrInput component and call onChange function whenever value is changed
<SelectOrInput
...
onChange={onChange}
/>
// call onChange in handleInputSelectChange method
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
setValue(value);
onChange(value);
} else {
setValue(value);
onChange(value);
}
};
Working example: https://codesandbox.io/s/dynamic-input-select-wk2je
With the great help of #Priyank Kachhela, I was able to find out the answer.
By Lifting the State to it's closest common ancestor as well as removing any Controller Component inside the child component.
App.js
Create state in parent component and initialize with default value and create function to change it's value
const [inputValue, setInputValue] = useState("null");
const onSubmit = (data) => {
// Stringify Object to always see real value, not the value evaluated upon first expanding.
// https://stackoverflow.com/questions/23429203/weird-behavior-with-objects-console-log
console.log(JSON.stringify(data, 4));
};
const onChange = (value) => {
setInputValue(value);
};
Wrap SelectOrInput with Controller and Pass onChange function, value as well as defaultValue to the Controller. Then use the render method and spread field on SelectOrInput Component.
<Controller
name={`contactPerson[0].salutation`}
defaultValue={defaultValues.salutation}
onChange={onChange}
value={inputValue}
control={control}
render={({ field }) => (
<SelectOrInput
{...field}
variant="outlined"
selectOptions={customSalutationOptions}
/>
)}
/>
components/SelectOrInput.js
Bubble / (Call) onChange Event Handler whenever value is changed from within the Child-(SelectOrInput) Component.
const handleInputSelectChange = (
event: React.ChangeEvent<{ value: unknown }>
): void => {
const value = event.target.value as string;
if (value === "") {
const newState = !showCustomInput;
setShowCustomInput(newState);
// Bubble / (Call) Event
onChange(value);
} else {
onChange(value);
}
};
const resetCustomInputToSelect = (event: React.MouseEvent<HTMLElement>) => {
const newState = !showCustomInput;
// Bubble / (Call) Event
onChange("null");
setShowCustomInput(newState);
};
Remove component internal Input State from the 'SelectOrInput'
Working Example
Revisions captured inside Gist
https://gist.github.com/kkroeger93/1e4c0fe993f1745a34fb5717ee2ff545/revisions
I'm using React with Material-ui and the Autocomplete component documented here - https://material-ui.com/components/autocomplete/ with downshift.
<Downshift id="downshift-options">
{({
clearSelection,
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
openMenu,
selectedItem,
}) => {
const {onSelect, onBlur, onChange, onFocus, ...inputProps} = getInputProps({
onChange: event => {
if (event.target.value === '') {
clearSelection();
}
},
onSelect: event => {
if (event.target.id) {
this.props.onSelect(event.target.value);
}
},
onFocus: openMenu,
placeholder: 'Type to search',
});
return (
<div className={classes.container}>
{renderInput({
fullWidth: true,
classes,
label: "Assigned Rider",
InputLabelProps: getLabelProps({shrink: true}),
InputProps: {onBlur, onChange, onFocus, onSelect},
inputProps,
})}
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{getSuggestions(this.props.suggestions, inputValue, {showEmpty: true}).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({item: suggestion.label}),
highlightedIndex,
selectedItem,
}),
)}
</Paper>
) : null}
</div>
</div>
);
}}
</Downshift>
With onSelect, I can retrieve the value of the selection. I'd like to be able to retrieve the key instead.
function renderSuggestion(suggestionProps) {
const { suggestion, index, itemProps, highlightedIndex, selectedItem } = suggestionProps;
const isHighlighted = highlightedIndex === index;
const isSelected = (selectedItem || '').indexOf(suggestion.label) > -1;
return (
<MenuItem
{...itemProps}
key={suggestion.uuid}
value={suggestion.uuid}
selected={isHighlighted}
component="div"
style={{
fontWeight: isSelected ? 500 : 400,
}}
>
{suggestion.label}
</MenuItem>
);
}
Here I set the uuid as the key for each selection.
My ultimate aim is to be able to make a selection and retrieve a uuid instead of the value itself.
Although I can use the value returned to match against a list of items, I want to be sure that if there end up being any duplicate entries, it doesn't cause problems.
Link to my full code for the component is here - https://github.com/theocranmore/bloodbike/blob/master/react_app/src/components/UsersSelect.js#L143
Thank you!
I don't know why you need an id but for my own getting the object itself will suffice.
<Autocomplete .....
onChange={(event, newValue) => {
console.log(newValue); //this will give you the selected value dictionary (source)
}}
You can retrieve the entire value and then access your desired key (option.id):
const options = [
{ id: 0, value: "foo" },
{ id: 1, value: "goo" },
];
<Autocomplete
options={options}
onChange={(event, option) => {
console.log(option.id); // 1
}}
renderInput={(params) => <TextField {...params} />}
/>;
when I click textbox text a menu opens, in that textbox I need to enter some value in the textbox.
after entering the value in the textbox if I hit enter, I should see a new chip with the entered value in it.
so I thought I will create an Onchange event and pass the values to the chip component
but the values are not getting passed.
I think I need to pass values an object to that array.
chipData.push(handleTextBox);
console.log("handleTextBox after push chipData--->", chipData);
can you tell me how to fix it.
providing my code snippet and sandbox below.
https://codesandbox.io/s/material-demo-99sf8
demo.js
const [chipData, setChipData] = React.useState([
{ key: 0, label: "Angular" },
{ key: 1, label: "jQuery" },
{ key: 2, label: "Polymer" },
{ key: 3, label: "React" },
{ key: 4, label: "Vue.js" }
]);
const [hideChip, setHideChip] = React.useState([false]);
const handleClick = event => {
setAnchorEl({ type: "icon", target: event.currentTarget });
};
const handleClickFilter = event => {
setAnchorEl({ type: "textbox", target: event.currentTarget });
};
const handleTextBox = event => {
console.log("handleTextBox event--->", event);
console.log("handleTextBox event.target.value--->", event.target.value);
};
console.log("handleTextBox handleTextBox--->", handleTextBox);
console.log("handleTextBox chipData--->", chipData);
chipData.push(handleTextBox);
console.log("handleTextBox after push chipData--->", chipData);
<Menu
id="simple-menu"
anchorEl={
anchorEl && anchorEl.type === "textbox" && anchorEl.target
}
open={Boolean(anchorEl && anchorEl.type === "textbox")}
onClose={handleClose}
>
<MenuItem>
<form
className={classes.container}
noValidate
autoComplete="off"
>
<TextField
id="standard-name"
label="Name"
className={classes.textField}
onChange={handleTextBox}
// value={values.name}
// onChange={handleChange('name')}
margin="normal"
/>
</form>
</MenuItem>
</Menu>
<Paper className={classes.root}>
{chipData.map(data => {
let icon;
console.log("setHideChip--->", setHideChip);
if (data.label === "React") {
icon = <TagFacesIcon />;
}
return (
<Chip
key={data.key}
icon={icon}
label={data.label}
onDelete={handleDelete(data)}
className={classes.chip}
/>
);
})}
</Paper>
try with this code .
<TextField
fullWidth={true}
id="standard-name"
label="Tags"
className={classes.textField}
onChange={handleChange('tag')}
onKeyDown={(e) => pushToarray(e)}
margin="dense"
/>
on entering you should see tag being added
function pushToarray(e){
if(e.key == 'Enter'){
e.preventDefault();
setValues({ tags : [...values.tags ,e.target.value] ,
tag : ''})
}
}
and normal handleChange of text change in the box :
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value });
console.log(values)
};
Initialize state like this I have initialized with empty array you can use keys:
const [values, setValues] = React.useState({
tag :'',
tags: [],
})
here is your handleDelete function to remove chips :
function handleDelete(item){
var rm = values.tags.indexOf(item);
values.tags.splice( rm, 1)
setValues( { tags : values.tags})
}
this is how your chip component should be
<Paper className={classes.root}>
{
values.tags && values.tags.map( (tag ,id) =>(
<Chip color="default" className={classes.chip} onDelete={() => handleDelete(tag)} key={id} label={tag}/>
))
}