I have a several ReactSelect components, and one piece of global state which is responsible for holding all my selected values in an array.
Select
<Select
styles={inputStyles}
className="basic-single"
classNamePrefix="select"
isClearable={true}
isSearchable={false}
placeholder={'Select Your Most Convenient Time Slot'}
options={newHoursArr}
isMulti={false}
onChange={(values) => handleChange(values)}
defaultValue={clientServiceReferral.selectedTimeSlots.map((referral) => (
referral.timeSlot === timeWithDate ? (
{ ['label']: referral.value, ['value']: referral.value }
) : null
))}
/>
handleChange function
const handleChange = (value) => {
const found = clientServiceReferral.selectedTimeSlots.find(element => element.timeSlot === timeWithDate);
if (found) {
clientServiceReferral.selectedTimeSlots.splice(found, 1)
}
const newValue = {
timeSlot: timeWithDate,
value: value.value
}
setClientServiceReferral({
...clientServiceReferral,
selectedTimeSlots: [...clientServiceReferral.selectedTimeSlots, newValue]
})
}
ReactSelect has an isClearable prop. Which allows users to clear the input with a button click. This returns a value of null when values is logged in the onChange function, but is there a way to return the actual value inside the select that is getting cleared when the clear button is clicked?
There's an optional second parameter passed to the onChange event. It's of this type:
export interface ActionMetaBase<Option> {
option?: Option | undefined;
removedValue?: Option;
removedValues?: Options<Option>;
name?: string;
}
Now, I've never used this library, but it looks like removedValue or removedValues could be helpful? idk.
Anyway, I got that from their docs. Hope it works out for you:
For anyone interested, Via Joshua Wood's answer, the value of any cleared item(s) can be found as so:
onChange={(values, removedValue) => handleChange(values, removedValue)}
const handleChange = (value, removedValue) => {
if (removedValue.action === 'clear') {
console.log('removed', removedValue.removedValues[0])
}
// removedValues returns an array
Related
I want to be able to visit the children <Textfield> of my form <Form> upon submit.
In each child hook object, I also want to trigger a certain function (eg., validate_field). Not sure if this possible in hooks? I do not want to use ref/useRef and forwardRef is a blurred concept to me yet (if that's of any help).
My scenario is the form has been submitted while the user did not touch/update any of the textfields so no errors were collected yet. Upon form submit, I want each child to validate itself based on certain constraints.
I tried looking at useImperativeHandle too but looks like this will not work on props.children?
Updated working code in:
https://stackblitz.com/edit/react-ts-jfbetn
submit_form(evt){
props.children.map(child=>{
// hypothetical method i would like to trigger.
// this is what i want to achieve
child.validate_field() // this will return "is not a function" error
})
}
<Form onSubmit={(e)=>submit_form(e)}
<Textfield validations={['email']}>
<Textfield />
<Textfield />
</Form>
Form.js
function submit_form(event){
event.preventDefault();
if(props.onSubmit){
props.onSubmit()
}
}
export default function Form(props){
return (
<form onSubmit={(e)=>submit_form(e)}>
{props.children}
</form>
)
}
So the Textfield would look like this
…
const [value, setValue] = useState(null);
const [errors, setErrors) = useState([]);
function validate_field(){
let errors = []; // reset the error list
props.validations.map(validation => {
if(validation === 'email'){
if(!some_email_format_validator(value)){
errors.push('Invalid email format')
}
}
// other validations (eg., length, allowed characters, etc)
})
setErrors(errors)
}
export default function Textfield(props){
render (
<input onChange={(evt)=>setValue(evt.target.value)} />
{
errors.length > 0
? errors.map(error => {
return (
<span style={{color:'red'}}>{error}</span>
)
})
: null
}
)
}
I would recommend moving your validation logic up to the Form component and making your inputs controlled. This way you can manage the form state in the parent of the input fields and passing in their values and onChange function by mapping over your children with React.cloneElement.
I don't believe what you're trying to do will work because you are trying to map over the children prop which is not the same as mapping over say an array of instantiated child elements. That is to say they don't have state, so calling any method on them wouldn't be able to give you what you wanted.
You could use a complicated system of refs to keep the state in your child input elements, but I really don't recommend doing that as it would get hairy very fast and you can just solve the issue by moving state up to the parent.
simplified code with parent state:
const Form = ({ children }) => {
const [formState, setFormState] = useState(children.reduce((prev, curr) => ({ ...prev, [curr.inputId]: '' }), {}));
const validate = (inputValue, validator) => {}
const onSubmit = () => {
Object.entries(formState).forEach(([inputId, inputValue]) => {
validate(
inputValue,
children.filter(c => c.inputId === inputId)[0].validator
)
})
}
const setFieldValue = (value, inputId) => {
setFormState({ ...formState, [inputId]: value });
};
const childrenWithValues = children.map((child) =>
React.cloneElement(child, {
value: formState[child.inputId],
onChange: (e) => {
setFieldValue(e.target.value, child.inputId);
},
}),
);
return (
<form onSubmit={onSubmit}>
{...childrenWithValues}
</form>
)
};
const App = () =>
<Form>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="email" inputId="foo"/>
<MyInput validator="password" inputId="foo"/>
</Form>
I still don't love passing in the validator as a prop to the child, as pulling that out of filtered children is kinda jank. Might want to consider some sort of state management or pre-determined input list.
I tried this solution, checking whether state variable is missing.
selectedOrganization?.barionId !== undefined
<TextFieldItem
primary="Barion ID"
required="**"
value={selectedOrganization?.barionId ?? ""}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
selectedOrganization.barionId = event.target.value;
setSelectedOrganization((preState) => ({
...preState,
barionId: event.target.value,
}));
}}
disabled={
selectedOrganization?.barionId !== undefined
}
/>
and has this state:
const [selectedOrganization, setSelectedOrganization] = useState<
OrganizationOut | undefined
>(undefined);
The challenge is that after the first character is filled into the textfield, the textfield gets disabled.
It's a little bit complicated. You have a field that maybe filled (or not). If you want to disable any field that have a previous value, I prefer this trick that use when I have same problem.
Define a variable for default value of the field
let defaultValueFromSomewhereElse : String = "";
If the defaultValueFromSomewhereElse has a value (ex. from an api), assign it. then
Define useState with default value
const [selectedOrganization, setSelectedOrganization] = useState<OrganizationOut>
(defaultValueFromSomewhereelse);
Disable field base on defaultValueFromSomewhereelse length.
<TextFieldItem
primary="Barion ID"
required="**"
value={defaultValueFromSomewhereElse}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
selectedOrganization.barionId = event.target.value;
setSelectedOrganization((preState) => ({
...preState,
barionId: event.target.value,
}));
}}
disabled={
defaultValueFromSomewhereElse.length > 0
}
/>
I am using ant design on my application. There is form.
useEffect(() => {
...props.client
let client = {
employeeId:props.client?.employee.id
};
form.setFieldsValue(client);
}, [props.client])
On update form 'employeeId' field is not sending to backend, but i want to set its value to null. Does someone now best way of that?
<Form.Item name="employeeId">
<Select allowClear>
{props.employees?.map((employee) => (
<Option key={employee.id} value={employee.id}>{employee.name}</Option>
))}
</Select>
</Form.Item>
I think your useEffect maybe a little buggy. I don't know what the ...props.client is doing there.
It might help to destructure the client prop as it is used so many times. Putting a snippet below to see if it works for you.
const Component = (props) => {
const { client, ...otherProps } = props;
useEffect(() => {
// if id is present, use it, else set null.
// Notice the use of ?? instead of || here.
// That is so that id with value 0 is not ignored as it is falsy in nature
form.setFieldsValue({ employeeId: client?.employee?.id ?? null });
}, [client])
}
So I have a fragment factory being passed into a Display component. The fragments have input elements. Inside Display I have an onChange handler that takes the value of the inputs and stores it in contentData[e.target.id]. This works, but switching which fragment is displayed erases their values and I'd rather it didn't. So I'm trying to set their value by passing in the state object to the factory. I'm doing it in this convoluted way to accomodate my testing framework. I need the fragments to be defined outside of any component and passed in to Display as props, and I need them all to share a state object.
My problem is setting the value. I can pass in the state object (contentData), but to make sure the value goes to the right key in the contentData data object I'm trying to hardcode it with the input's id. Except contentData doesn't exist where the fragments are defined, so I get an error about not being able to reference a particular key on an undefined dataObj.
I need to find a way to set the input values to contentData[e.target.id]. Thanks.
File where fragments are defined. Sadly not a component.
const fragments = (onChangeHandler, dataObj) => [
<Fragment key="1">
<input
type="text"
id="screen1_input1"
onChange={onChangeHandler}
value={dataObj['screen1_input1']} // this doesn't work
/>
one
</Fragment>,
<Fragment key="2">
<input
type="text"
id="screen2_input1"
onChange={onChangeHandler}
value={dataObj['screen2_input1']}
/>
two
</Fragment>
]
Display.js
const Display = ({ index, fragments }) => {
const [contentData, setContentData] = useState({})
const onChange = e => {
// set data
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>{fragments(onChange, contentData)[index]}</Fragment>
);
};
After conversing with you I decided to rework my response. The problem is mostly around the implementation others might provide in these arbitrary fragments.
You've said that you can define what props are passed in without restriction, that helps, what we need to do is take in these nodes that they pass in, and overwrite their onChange with ours, along with the value:
const RecursiveWrapper = props => {
const wrappedChildren = React.Children.map(
props.children,
child => {
if (child.props) {
return React.cloneElement(
child,
{
...child.props,
onChange: props.ids.includes(child.props.id) ? child.props.onChange ? (e) => {
child.props.onChange(e);
props.onChange(e);
} : props.onChange : child.props.onChange,
value: props.contentData[child.props.id] !== undefined ? props.contentData[child.props.id] : child.props.value,
},
child.props.children
? (
<RecursiveWrapper
ids={props.ids}
onChange={props.onChange}
contentData={props.contentData}
>
{child.props.children}
</RecursiveWrapper>
)
: undefined
)
}
return child
}
)
return (
<React.Fragment>
{wrappedChildren}
</React.Fragment>
)
}
const Display = ({ index, fragments, fragmentIDs }) => {
const [contentData, setContentData] = useState(fragmentIDs.reduce((acc, id) => ({
...acc, [id]: '' }), {}));
const onChange = e => {
setContentData({
...contentData,
[e.target.id]: e.target.value
})
};
const newChildren = fragments.map(fragment => <RecursiveWrapper onChange={onChange} ids={fragmentIDs} contentData={contentData}>{fragment}</RecursiveWrapper>);
return newChildren[index];
};
This code outlines the general idea. Here we are treating fragments like it is an array of nodes, not a function that produces them. Then we are taking fragments and mapping over it, and replacing the old nodes with nodes containing our desired props. Then we render them as planned.
I'm developing an app in React.Js and for some reason it is not saving the value that is selected in the select.
const App = () => {
// is obtained from another api: REQUEST_API_GET
const Dates = dates.map(item => ({
value: item.Name,
label: item.Name,
}));
const [date, setDate] = React.useState({
Name: ''
});
function handleChangeDate(event) {
setDate({
Name: event.value
})
}
const addDate = (date) => {
axios.post(`${REQUEST_API_POST}`, {date});
};
return (
<>
<div>
<NoSsr>
<Select
classes={classes}
styles={selectStyles}
inputId="date"
TextFieldProps={{
label: 'Date',
InputLabelProps: {
htmlFor: 'date',
shrink: true,
},
placeholder: 'Search',
}}
options={date}
components={components}
value={date.Name}
onChange={handleChangeDate}
/>
</NoSsr>
<a onClick={() => addDate()}>add</a>
</div>
</>
)
}
export default App;
It seems to take the value when I select it, you can see in handleChangeDate if put a console.log, but in addDate it is not taking it.
How can I fix it, suggestions?
The variable date you're using in addDate is not the hook.
Just remove date parameter.
const addDate = () => { // <-- no date in input
axios.post(`${REQUEST_API_POST}`, {date});
};
Just at looking at your code it is obvious that you are not actually using the proper data in your addDate handler.
You need to pass the state value in the param or even better directly in your callback as follows:
const addDate = () => {
axios.post(`${REQUEST_API_POST}`, {date}); // here your date is your state!
};
Since you are not passing any parameters, you can improve your callback in your onClick like that too :
<a onClick={addDate}>add</a>
Another detail, it's more common to have your object properties as lowercase values hence Name to name.
When calling addDate() function you are not passing any argument, but in your parameter you have specified date as a parameter (that too without any default value). I think you are trying to access the value of date, which is a state, and state can be accessed anywhere in the function, so no need to pass it as a parameter.
enter code here
const addDate = () => {
axios.post(`${REQUEST_API_POST}`, {date});
};
Use this and it will work fine
You can also pass date (state) as an argument in you function addData, something
like this(addData(date)) and this will work too. but first approach is better