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])
}
Related
I'm reusing a couple of external components to create my custom Combobox in strapi app.
Values are received from server so I need to add options dynamically.
Currently there is the following code:
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected
}) {
const [value, setValue] = useState('');
const combo = (<Combobox label="Country" value={value} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value="{entry.id}">{entry.name}</ComboboxOption>
);
})}
</Combobox>);
// setValue(valueSelected)
return combo;
}
And everything goes good until I try so set 'selected' option basing on another set of data. In static world I could just say useState(valueSelected) and it will work. But as code generated dynamically, there is no related option yet, so I get failure like "Failed to get 'props' property of undefined".
I tried to put this combobox into a variable and set state between creation and returning it (commented setValue line before the return statement) but then app gets in a loop and returns "Too many re-renders".
Does anyone has an idea of how to change/rewrite this to be able to set selected value for dynamically created combobox?
So I assume that the values are dynamically fetched and passed to the ComboboxCustom.
I think you can add setValue(valueSelected) inside an useEffect.
onChange of the prop valueSelected.something like,
useEffect(() => {
setValue(valueSelected)
}, [valueSelected])
Also handle the return when the value is not yet loaded. like before doing valuesList.map, first check if valueList ? (render actual) : (render empty)
Hope this helps!!
Thanks,
Anu
Finally I got working solution based on answer from #Anu.
Cause valuesList is got as GET-request from another hook, I have to check values are already present (first hook hit gives [] yet) and bind Combobox state updating to change of valuesList also. Though I don't fell like this solution is perfect.
import React, { useState, useEffect } from "react";
import {
Combobox,
ComboboxOption
} from "#strapi/design-system";
export default function ComboboxCustom({
valuesList,
valueSelected,
}) {
const [value, setValue] = useState('');
let combo = null;
useEffect(() => {
if(combo && combo?.props?.children?.length > 0 && valuesList.length > 0) {
setValue(valueSelected)
}
}, [valueSelected, valuesList])
combo = (<Combobox label="Country" value={value?.toString()} onChange={setValue}>
{valuesList.map((entry) => {
return(
<ComboboxOption value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>);
return combo;
}
After that I decided avoid creating custom component based on already published as I'll need to add and process event listeners that are added for us in the existing components. So I placed this code directly into my modal and it also works:
const [countries, setCountries] = useState([]);
const [deliveryCountryValue, setDeliveryCountryValue] = useState('');
useEffect(async () => {
const countriesReceived = await countryRequests.getAllCountries();
setCountries(countriesReceived);
}, []);
useEffect(() => {
// If there is no selected value yet, set the one we get from order from server
const valueDelivery = deliveryCountryValue != '' ? deliveryCountryValue : order.country?.id;
if(countries.length > 0) {
setDeliveryCountryValue(valueDelivery);
order.country = countries.find(x => x.id == valueDelivery);
}
}, [deliveryCountryValue, countries])
<Combobox key='delivery-combo' label="Country" value={deliveryCountryValue?.toString()} onChange={setDeliveryCountryValue}>
{countries.map((entry) => {
return(
<ComboboxOption key={'delivery'+entry.id} value={entry?.id?.toString()}>{entry.name}</ComboboxOption>
);
})}
</Combobox>
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
So taking a look at the Apollo useMutation example in the docs https://www.apollographql.com/docs/react/data/mutations/#tracking-loading-and-error-states
function Todos() {
...
const [
updateTodo,
{ loading: mutationLoading, error: mutationError },
] = useMutation(UPDATE_TODO);
...
return data.todos.map(({ id, type }) => {
let input;
return (
<div key={id}>
<p>{type}</p>
<form
onSubmit={e => {
e.preventDefault();
updateTodo({ variables: { id, type: input.value } });
input.value = '';
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Update Todo</button>
</form>
{mutationLoading && <p>Loading...</p>}
{mutationError && <p>Error :( Please try again</p>}
</div>
);
});
}
This seems to have a major flaw (imo), updating any of the todos will show the loading state for every single todo, not just the one that has the pending mutation.
And this seems to stem from a larger problem: there's no way to track the state of multiple calls to the same mutation. So even if I did want to only show the loading state for the todos that were actually loading, there's no way to do that since we only have the concept of "is loading" not "is loading for todo X".
Besides manually tracking loading state outside of Apollo, the only decent solution I can see is splitting out a separate component, use that to render each Todo instead of having that code directly in the Todos component, and having those components each initialize their own mutation. I'm not sure if I think that's a good or bad design, but in either case it doesn't feel like I should have to change the structure of my components to accomplish this.
And this also extends to error handling. What if I update one todo, and then update another while the first update is in progress. If the first call errors, will that be visible at all in the data returned from useMutation? What about the second call?
Is there a native Apollo way to fix this? And if not, are there options for handling this that may be better than the ones I've mentioned?
Code Sandbox: https://codesandbox.io/s/v3mn68xxvy
Admittedly, the example in the docs should be rewritten to be much clearer. There's a number of other issues with it too.
The useQuery and useMutation hooks are only designed for tracking the loading, error and result state of a single operation at a time. The operation's variables might change, it might be refetched or appended onto using fetchMore, but ultimately, you're still just dealing with that one operation. You can't use a single hook to keep track of separate states of multiple operations. To do that, you need multiple hooks.
In the case of a form like this, if the input fields are known ahead of time, then you can just split the hook out into multiple ones within the same component:
const [updateA, { loading: loadingA, error: errorA }] = useMutation(YOUR_MUTATION)
const [updateB, { loading: loadingB, error: errorB }] = useMutation(YOUR_MUTATION)
const [updateC, { loading: loadingC, error: errorC }] = useMutation(YOUR_MUTATION)
If you're dealing with a variable number of fields, then we have to break out this logic into a separate because we can't declare hooks inside a loop. This is less of a limitation of the Apollo API and simply a side-effect of the magic behind hooks themselves.
const ToDo = ({ id, type }) => {
const [value, setValue] = useState('')
const options = { variables = { id, type: value } }
const const [updateTodo, { loading, error }] = useMutation(UPDATE_TODO, options)
const handleChange = event => setValue(event.target.value)
return (
<div>
<p>{type}</p>
<form onSubmit={updateTodo}>
<input
value={value}
onChange={handleChange}
/>
<button type="submit">Update Todo</button>
</form>
</div>
)
}
// back in our original component...
return data.todos.map(({ id, type }) => (
<Todo key={id} id={id} type={type] />
))
I'm currently working on a react component with a simple form (no redux, just using react-bootstrap for styling)
I'm pulling data from a database in the following form:
[
{
"id":"123",
"name":"Amy Pond",
"age":"22",
"reputation":22000
},
{
"id":"124",
"name":"Clara Oswald",
"age":"24",
"reputation":35000
}
...
]
and putting it into an object:
let userlist = [];
userlist = addUsers(this.state.users);
I used map to populate a dropdown based on this data, and can correctly set the state of the user to the selected one from the drop down:
<FormControl
id = "user"
componentClass="select"
onChange={this.handleChange}
>
{userlist.map((r , i) =>
<option
key={i}
value={r.name}>
{r.name}
</option>
)}
</FormControl>
using handleChange()
handleChange(event) {
this.setState({
value: event.target.value,
// reputation: ???? // here's where I'm running into issues
// age: ??????? // and here
});
My problem is, I need the user's age and reputation in the handleChange function to set their states accordingly. I've tried passing just r instead of r.name but I get [object Object] back.
console.log ("Handle Change Value: " + event.target.value);
if I try event.target.value.name I get undefined back.
The only thing that has sort of worked so far, is using JSON.stringify, but I feel like that's not the right way to do this. I've also thought about just searching through the objects once I've gotten just the name, but if there was a lot of objects I think that would be extremely inefficient? I'm also not sure how to do that.
Can anyone please help me find a way to pass these two extra values so I can set the state? Is map even the best way to create the options? I honestly feel like this is such a simple thing, but I'm still new to react and I've been struggling with this for far too long!
Any ideas / changes / comments / way to make this better would be much appreciated! I realize that perhaps using something like redux-form would make this easier, but I'm hoping there's a simple way to do it without that just for now.
Edit:
onChange should have been set to onChange = {this.handleChange}, just pasted the wrong code from my attempts to troubleshoot. I'm binding this in the constructor:
constructor(props) {
super(props);
...
this.handleChange = this.handleChange.bind(this);
}
Use the user ID as the value in each option, then in handleChange find the user in your userlist to access all the user's properties:
<FormControl
id = "user"
componentClass="select"
onChange={this.handleChange.bind(this)}
>
{userlist.map((r , i) =>
<option
key={i}
value={r.id}>
{r.name}
</option>
)}
</FormControl>
handleChange(event) {
const userId = event.target.value;
const user = userlist.find(u => u.id === userId);
this.setState({
value: user
});
}
Also, the way you are passing the onChange property might cause issues. It's hard to tell without seeing all your code, but make sure that you pass in a reference to your handleChange function (see above in my answer) rather than calling it right there (as in your question: onChange={this.handleChange()}, unless your this.handleChange function returns a function).
It could be a problem with the way you've set your event listener, settings onChange={this.handleChange()} would cause the listener to fire right away. Using onChange={this.handleChange} correctly sets the listener.
<FormControl
id = "user"
componentClass="select"
onChange={this.handleChange}>
{userlist.map((r , i) =>
<option
key={i}
value={r.name}>
{r.name}
</option>
)}
</FormControl>
See here for more on events.
You can then use find() in your event handler to locate the user:
handleChange(event) {
let user = users.find(u => u.name === event.user.name);
this.setState({user: user});
}
you can use option tag attribute and json to encode/decode like this:
<FormControl
id="user"
componentClass="select"
onChange={this.handleChange.bind(this)}
>
{userlist.map((r, i) =>
<option
key={i}
value={r.id}>
{r.name}
data={JSON.stringify(r)}
</option>
)}
</FormControl>;
handleChange(event)
{
if (e.target.options && e.target.selectedIndex) {
const user = e.target.options[e.target.selectedIndex].getAttribute('data');
this.setState({
value: user
});
}
}
I am attempting to use react-widgets with onSearch with Multiselect. I can see that onSearch gets called with searchTerm. But, the response never becomes part of the options/data for Multiselect.
I've tried returning the results in an Array or in an object with the key data to the Array.
const getOptions = searchTerm => {
return [{ _id: 1, username: 'Bob' }];
}
const TagItem = ({ username }) => <span>{username}</span>
const MultiSelectUsers = ({ input, ...rest }) =>
<Multiselect {...input}
onBlur={() => input.onBlur()}
value={input.value || []}
valueField="_id"
tagComponent={TagItem}
itemComponent={TagItem}
onSearch={getOptions}
{...rest}
/>
Bonus: How do I use onSearch with Promise?
A couple of things need to happen here.
1) onSearch merely calls a callback, ie, it updates nothing. The update needs to be done via data. (Eg, use state in MultiSelectUsers)
2) The trick is that #1 won't work, unless you set filter={false} in Multiselect