set default value for select menu from mongodb - reactjs

I have a dropdown list which is populated from a database. The first option is 'none' (actual record in database with objectId) which should be the default option and only needs to be changed if the user wants to, otherwise it should just use that initial value when submitting the form. However, even though it is selected and has a valid objectId, I still get a validation error saying the field is empty. The validation error only goes away if I select something else from the select menu or select something else and then select 'none' again. I am using Joi-browser for validation.
schema = {
subcategoryId: Joi.string()
.required()
.label("Subcategory"),
}
This is the select menu:
<Form onSubmit={this.handleSubmit}>
<Form.Group controlId="subcategoryId">
<Form.Label>Sub-category</Form.Label>
<Form.Control
as="select"
name="subcategoryId"
value={this.state.data.subcategoryId}
onChange={this.handleChange}
error={this.state.errors.subcategory}
>
{this.state.subcategories.map(subcategory => (
<option key={subcategory._id} value={subcategory._id}>
{subcategory.name}
</option>
))}
</Form.Control>
{this.state.errors.subcategoryId && (
<Alert variant="danger">
{this.state.errors.subcategoryId}
</Alert>
)}
</Form.Group>
And here is my state:
state = {
data: {
name: "",
description: "",
categoryId: "",
subcategoryId: "",
price: ""
},
categories: [],
subcategories: [],
errors: {}
};
const { data: subcategories } = await getSubcategories();
this.setState({ subcategories });
And this is the html output of the dropdown's first field which I want selected by default:
<option value="5d4b42d47b454712f4db7c67">None</option>
The error I get back is that the category Id cannot be empty, yet each option in the select menu has a value. I am new to react but perhaps the value is only actually assigned upon change?

You need to edit componentDidMount. After you get your subcategories, you'll need to set the state to of this.state.data.subcategoryId to one of the categories. This is because you're using a controlled component. Otherwise, it'll still be set to "", which isn't one of the valid values for the <select> component, and likely why it's failing validation.
async componentDidMount() {
// getting a copy of this.state.data so as not to mutate state directly
const data = { ...this.state.data };
const { data: subcategories } = await getSubcategories();
// filter the array to a new array of subcategories that have the name === 'none'
const arrayOfSubcategoriesWhereNameIsNone = subcategories.filter(i => i.name === 'none');
const getIdOfFirstElementOfArray = arrayOfSubcategoriesWhereNameIsNone [0]._id;
//set getIdOfFirstElementOfArray equal to the function's local copy of this.state.data.subcategoryId
data.subcategoryId = getIdOfFirstElementOfArray;
// update the state with the mutated object (the function's local copy of this.state.data)
this.setState({ subcategories, data });
}

Related

Error when trying to change MUI (Material UI) checkbox value via state and onChange in React/NextJS

The error is: TypeError: filters.brand.find is not a function
and it says that it happened here:
checked={
173 | filters.brand.find((item) => item === brand)
| ^
174 | ? true
175 | : false
176 | }
Below is what I'm trying to achieve:
I'm building an e-commerce site using NextJS and amongst other features, users can choose to view products by category or subcategory. On either of these pages, users can choose to refine the results even further using filters. One of these filters should allow the user to display only the items of a certain brand/s.
These brands are presented to the user as checkboxes. So a checked checkbox should mean "include this brand" otherwise do not:
<FormGroup>
{brands.map((brand) => (
<FormControlLabel
control={
<Checkbox
size="small"
name={brand}
checked={
filters.brand.find((item) => item === brand)
? true
: false
}
onChange={() => handleBrandChange(brand)}
/>
}
label={brand}
key={brand}
/>
))}
</FormGroup>
btw: I tried using "includes" instead of "find" above but still ran into the same problem.
The initial value for filter.brand comes from the URL's query string.
const { query, ...router } = useRouter();
const [filters, setFilters] = useState({
brand: query.brand ? (typeof query.brand === "string" ? [query.brand] : query.brand) : [],
});
I check if the query string has "brand", which could either come back as a string or an array. If it is there, I check its type. If it's a string, I set filter.brand's value to an array with that string inside otherwise I take it as is. If the query string does not have a brand, I assign filter.brand an empty array.
btw, I am leaving out most of the code that is of no relevance to this problem.
Below is how I tried to handle the change (when the user clicks on a brand checkbox):
const handleBrandChange = (brand) => {
const brandArray = filters.brand;
if (brandArray.find((item) => item === brand)) {
setFilters({ ...filters, brand: brandArray.filter((item) => item !== brand) });
} else setFilters({ ...filters, brand: brandArray.push(brand) });
};
and below is what should happen every time the state changes:
useEffect(() => {
const urlObject = {
query: {
category: query.category,
sortBy: filters.sortBy,
maxPrice: filters.maxPrice,
},
};
if (filters.brand.length > 0) urlObject.query.brand = filters.brand;
if (query.subcategory) {
urlObject.pathname = "/[category]/[subcategory]";
urlObject.query.subcategory = query.subcategory;
} else {
urlObject.pathname = "/[category]";
}
router.push(urlObject);
}, [filters, query.subcategory, query.category]);
Sorry for the messy question. I am not used to asking questions on here (this is only my second time), I have not been coding for that long and English is not my first language. Any help would be greatly appreciated.

Populating a Material-UI dropdown with Redux store data

I am trying to get my Redux store fields to automatically populate a method I have imported. Am I going about this the right way in order to get this done? Do I need to create a mapping options for each field?
I have each of my dropdowns inserted with a PopulateDropdown list and the fields in each of them but I need them split as per the id and text.
Am I accessing my redux store correctly below? I have the array declared on up my function component by using const fields = useSelector(state => state.fields);
Update
I have the method inserted into where the dropdowns should be however I don't think I am accessing the data correctly which is causing the problem. The fields array has been de-structured into the six different fields for each dropdown and different mappingOptions have been created for each one.
What do I need to do to get the data into the method? the examples I have seen have static arrays declared on the component rather than use the Redux store.
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList, currentList, regionList, diveTypeList, visibilityList, diveSpotList } = fields;
populateDropdown method that I have imported
export const PopulateDropdown = ({ dataList = [], mappingOptions, name, label }) => {
const { title, value } = mappingOptions;
return (
<FormControl style={{ width: 200 }} >
<InputLabel id={label}>{label}</InputLabel>
<Select labelId={label} name={name} >
{dataList.map((item) => (
<MenuItem value={item[value]}>{item[title]}</MenuItem>
))}
</Select>
</FormControl>
);
};
imported dropdown menu
<PopulateDropdown
dataList={diveType}
mappingOptions={mappingOptions}
name="fieldName"
label="Select dive type"
value={dive.typeID}
onChange={handleChange}/>
Update
I have updated my action, reducer and populateFields method however I am still having trouble mapping the redux data to my two property fields. In the Redux tree the fields should be under the fields.data.fieldlists as they print when I console log them.
What way should I be populating them into the titleProperty etc? It is currently looking like it might be populating but a large box drops downs that I can't see any values inside.
// select user object from redux
const user = useSelector(state => state.user);
// get the object with all the fields
const fields = useSelector(state => state.fields);
// can destructure individual fields
const { diveSchoolList = [],
currentList = [],
regionList = [],
diveTypeList = [],
visibilityList = [],
diveSpotList = [],
marineTypeList = [],
articleTypeList = []
} = fields;
.........
<PopulateDropdown
dataList={fields.data.diveTypeList} // the options array
titleProperty={fields.data.diveTypeList.diveTypeID} // option label property
valueProperty={fields.data.diveTypeList.diveType} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange(setDive.typeID)} // update state on change
/>
Your PopulateDropdown component looks correct except that we need it to use the value and onChange that we passed down as props.
My personal preference would be to use separate properties valueProperty and titleProperty instead of passing a single mappingOptions. That way you don't need to create objects for every dropdown, you just set the two properties in your JSX. You could get rid of this part entirely if you normalized your data such that the elements of every list have the same properties id and label.
<PopulateDropdown
dataList={diveTypeList} // the options array
titleProperty={"diveTypeId"} // option label property
valueProperty={"diveType"} // option value property
label="Dive Type Name" // label above the select
placeholder="Select dive type" // text show when empty
value={dive.typeID} // get value from state
onChange={handleChange("typeId")} // update state on change
/>
export const PopulateDropdown = ({
dataList = [],
valueProperty,
titleProperty,
label,
...rest // can just pass through all other props to the Select
}: Props) => {
return (
<FormControl style={{ width: 200 }}>
<InputLabel id={label}>{label}</InputLabel>
<Select {...rest} labelId={label}>
{dataList.map((item) => (
<MenuItem value={item[valueProperty]}>{item[titleProperty]}</MenuItem>
))}
</Select>
</FormControl>
);
};
It looks like the ids in currentId are actually a number, so at some point in your code you will want to convert that with parseInt because e.target.value is always a string, though maybe the backend can handle that.
Loading the API Data
It looks like you figured out how to fetch all of the fields in one API call which is great. You are saving it to a property fields on the fields reducer which creates the structure state.fields.fields. Since you are replacing the whole state, you can just return the whole thing as the entire slice state.
You can initialize your state object with empty arrays, or you can use an empty object {} as your initial state and fallback to an empty array when you destructure the arrays off of it, like const {diveSchoolList = [], currentList = []} = fields.
export const requireFieldData = createAsyncThunk(
"fields/requireData", // action name
// don't need any argument because we are now fetching all fields
async () => {
const response = await diveLogFields();
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { fields } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (fields.didLoadData) {
// return false to cancel execution
return false;
}
}
}
);
const fieldsSlice = createSlice({
name: "fields",
initialState: {
currentList: [],
regionList: [],
diveTypeList: [],
visibilityList: [],
diveSpotList: [],
diveSchoolList: [],
marineTypeList: [],
articleTypeList: [],
didLoadData: false,
},
reducers: {},
extraReducers: {
// picks up the pending action from the thunk
[requireFieldData.pending.type]: (state) => {
// set didLoadData to prevent unnecessary re-fetching
state.didLoadData = true;
},
// picks up the success action from the thunk
[requireFieldData.fulfilled.type]: (state, action) => {
// want to replace all lists, there are multiple ways to do this
// I am returning a new state which overrides any properties
return {
...state,
...action.payload
}
}
}
});
So in the component we now only need to call one action instead of looping through the fields.
useEffect(() => {
dispatch(requireFieldData());
}, []);

Dynamically populate a Material UI Select Component, depending on other states

I'm currently stuck with the following scenario.
I have two select components from Material-UI.
Select component one contains car brands, component two contains car models.
Desired behaviour
The car brands get fetched from the server and the list for Select "brands" gets populated.
Select "models" is still disabled, until a brand is selected.
Brand gets selected, client is fetching all models from this brand from server
Options for the Select 'models' get populated and the component becomes enabled
I store the fetched brand in a redux state.
I store the fetched model in a redux state.
I store the selected brand and model in a redux state.
{
brand:{
brands:{
brands:[
{
_id: 1,
name: "vw"
},
{
_id: 2,
name: "bmw"
}
]
}
},
data:{
form: {
brandId: '',
modelId: '',
errors: {}
},
models:{}
}
}
redux-states
Current behaviour
Car makers get fetched from server and the options for Select component "makers" gets populated as expected.
Select "models" is still disabled, as it should be.
I click on the "brands" select and see the available brands, as expected.
I select a brand and the following happens
"brands" select doesn't show any value and if i click on it again, the list is empty
the following warning appears
SelectInput.js:342 Material-UI: You have provided an out-of-range value
Project setup
let onChange = event => {
dispatch({
type: 'UPDATE_USER_INVITE_FORM',
field: event.target.id,
payload: event.target.value
});
};
...
return(
...
<FormControl
variant="outlined"
className={classes.formControl}
error={form.errors.brandId? true : false}
disabled={brandRenderList.length < 0}>
<InputLabel id="brand-label">Brand</InputLabel>
<Select
labelId="brand-label"
id="brandId"
label="Brand"
value={form.brandIdId}
onChange={(e) => { e.target.id = 'companyId'; onChange(e) }}
>
{brandRenderList}
</Select>
<FormHelperText>{form.errors.brandId}</FormHelperText>
</FormControl>
<FormControl
variant="outlined"
className={classes.formControl}
error={form.errors.modelId? true : false}
disabled={modelRenderList.length < 0}>
<InputLabel id="model-label">Model</InputLabel>
<Select
labelId="model-label"
id="modelId"
label="Model"
value={form.modelId}
onChange={(e) => { e.target.id = 'modelId'; onChange(e) }}
>
{modelRenderList}
</Select>
<FormHelperText>{form.errors.modelId}</FormHelperText>
</FormControl>
...
)
MyComponent.js
const selectedBrandId = useSelector(state => state.data.form.brandId);
// fetch brands
const brands = useSelector(state => state.brand.brands);
useEffect(() => {
if (Object.keys(brands).length === 0) {
dispatch(getBrands())
} else if (brandRenderList.length <= 0) {
brands.brands.forEach(brand => {
brandRenderList.push(<MenuItem value={brand._id} key={brand._id}>{brand.name}</MenuItem>)
})
}
}, [brands])
// fetch models
const models = useSelector(state => state.model.models);
useEffect(() => {
if(!selectedBrandId ){return}
else if (Object.keys(models).length === 0) {
dispatch(getModels({brandId: selectedBrandId }))
} else if (modelRenderList.length <= 0) {
models.models.forEach(model => {
modelRenderList.push(<MenuItem value={model._id} key={model._id}>{model.name}</MenuItem>)
})
}
}, [brands])
ParentComponent.js
If I put a static brandId, everything works just as desired. Obviously I want a dynamic one, which reacts to the user input in the brand select component.
// this works
const selectedBrandId = '6018f90b94a59215703adba0';
// this doesn't
const selectedBrandId = useSelector(state => state.data.form.brandId);
I appreciate any feedback :)
Hi mate i feel you i have been stuck for the same things few week ago at work !
I'm asking myself who had texted this nonsense warning alert: "You have provided an out-of-range value " you get this error just because the selectbox needs a defaultValue property. Insane bullsh** i agree and not really helpful for debug !
Hope you can fix it and keep coding have a great day !

react select is not picking up my selected value using typescript

I am sorry if my question is silly, I am fairly new to Reactjs,
Please do not tell me to useHooks as I can't use it in my current project.
I am working on a form and on it's validations.
my relevant code:
//part of my state
this.state = {
form: {
country: null,
zipCode: "",
},
formErrors: {
country: null,
},
countryList: Array<string>(),
};
}
<Select name="country" defaultValue={countryOptions[1]} options={countryOptions} value={form.country} onChange={(e) => {
this.handleCountryChange({value : e.value, label: e.value })
}}
/>
{this.state.formErrors.country && (
<span className="err">{this.state.formErrors.country}</span>
)}
handleCountryChange(e : countrySelection){
const selectedCountry = e.value;
console.log(selectedCountry);
var formAux = { ...this.state };
formAux.form.country = selectedCountry;
}
but sadly my select keeps looking like this:
Even after I pick a country and I can see it with the console log above. What is missing here in order for the select to pick up the value?
You have to do this something like this
How to set a default value in react-select
Here they did not use defaultValue property
As well as check other properties which they used.
As well as refer this answer of this question too
react select not recognizing default value
You are not setting the state correctly, you can not mutate the state like that. You need to use setState API. See docs: https://reactjs.org/docs/react-component.html#setstate
In your case the event handler should be like this:
handleCountryChange(e : countrySelection){
const selectedCountry = e.value;
// you said this works correctly
console.log(selectedCountry);
this.setState(currentState => ({
// spread the entire state so that other keys in state remain as they are
...currentState,
form: {
// spread state.form so that other keys in state.form remain as they are
...currentState.form,
// only update state.form.country
country: selectedCountry
}
}));
}
Please read the code comments, that should help you understand.

failed to get some values from dynamic multiple input fields using reactjs

i have implemented dynamic input fields that are created whenever u click add Product button but i dont why i can only pick itemAmount values but not input values for productId and itemQuantity.
iam using react v16.0.2
productItems: Array(2)
0: {"": "450000"} //productId is empty
1: {"": "670000"} //why does it only pick itemAmount but not the quantity and productId
i happen to use react-bootstarp for styling.
constructor(){
this.state = {
invoice_notes: '',
itemValues: [] //suppose to handle the productItems that are being added.
}
//for the product items
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
}
addProductItem(){
const item = {
productId: '',
itemQty:'',
itemAmount: ''
}
this.setState({itemValues: [...this.state.itemValues, item]})
}
createItemsUI(){
//Use #array.map to create ui (input element) for each array values.
//while creating the fields, use a remove button with each field,
return this.state.itemValues.map((elem,i)=> //recall arrow functions do not need return statements like {}
<div key = {i}>
<FormGroup controlId="formControlsSelect">
<ControlLabel>Product</ControlLabel>
<FormControl
componentClass="select"
name = "productId"
bsClass="form-control"
value = {this.state.itemValues[i].productId}
onChange = {this.handleItemChange.bind(this,i)}>
<option value = "" disabled>{'select the product'}</option>
{productOptions.map((item,i)=>{
return(
<option
key = {i} label= {item} value = {item}
>
{item}
</option>
)
})}
</FormControl>
</FormGroup>
<FormInputs
ncols={["col-md-4","col-md-4"]}
proprieties={[
{
label: "Quantity",
type: "number",
bsClass: "form-control",
defaultValue:this.state.itemValues[i].itemQty,
onChange: this.handleItemChange.bind(this,i)
},
{
label: "Amount",
type: "number",
bsClass: "form-control",
defaultValue: this.state.itemValues[i].itemAmount,
onChange: this.handleItemChange.bind(this,i)
}
]}
/>
<Button onClick = {this.removeProductItem.bind(this,i)}>Remove</Button>
</div>
)
}
}
please i have read many similar questions so i think iam on the right track.. I prefer to be inquisitive
So after #steve bruces comment, i made some changes to the handleItemChange function but i happen to get almost the same behaviour of not picking the productId and itemQty input values.
handleItemChange(i,evt){
const name = evt.target.getAttribute('name');
console.log('let see name', name) //i getting the name of the atrribute --> productId, itemQty, ItemAmount
const items = [...this.state.itemValues]
items[i] = {[name]: value}
this.setState({itemValues: items})
}
This is what happens if i try using setState({items}) as suggested by similar questions
items[i] = {[name]: value}
this.setState({items})
RESULT of the productItems when u click the submit button
productItems: Array(2)
0: {productId: "", itemQty: "", itemAmount: ""}
1: {productId: "", itemQty: "", itemAmount: ""}
but when i use this.setState({itemValues: items}) atleast i can get the last value of the itemAmount
productItems: Array(2)
0: {itemAmount: "25000"}
1: {itemAmount: "45000"}
length: 2
Any help is highly appreciated
I believe your error is coming up in your handleItemChange(evt,i) function.
It looks like you want to return "placeholder" by destructuring evt.target. This does not work because evt.target doesn't understand "name". I think you want to try evt.target.attribute('name') to return the name. evt.target.value will return the value of that input and evt.target.name does not exist so it returns undefined.
A small tip. You no longer have to bind functions in your constructor. You can can get rid of your constructor and update your functions to be arrow functions.
For example this:
handleItemChange(i,evt){
const {name,value} = evt.target;
const items = [...this.state.itemValues]
items[i] = {[name]: value}
//this.setState({items}) //it just has empty values
this.setState({itemValues: items}) //works but empty for productId n amount
becomes this:
handleItemChange = (i,evt) => {
// const {name,value} = evt.target;
//updated that to this here
const { value } = evt.target;
const name = evt.target.attribute("name");
...
And the call to in your onChange would look like this:
onChange = {() => { this.handleItemChange(evt,i)}
you must add the () => in your onChange so the handleItemChange is not invoked on every render before it's actually changed. It's invoked the moment you pass the params evt, and i. So the () => function is invoked onChange and then handleItemChange(evt,i) is invoked.
Hope this helps.

Resources