I want to add a new column in mui-datatable every time a button is pressed. However datatables doesn't seem to be rendering it. However, if I clicked on the add column button, and then select the dropdown, the new columns appear. Furthermore the selected dropdown value does not seem to be reflected as well . I've narrowed the issue down to using const [columns,setColumns] = useState(...) for the column, but without that, I can't add any new columns dynamically at all. Appreciate any help to get me out of this pickle, thank you.
https://codesandbox.io/s/unruffled-sun-g77xc
const App = () => {
function handleChange(event) {
setState({ value: event.target.value });
}
const [state, setState] = useState({ value: "coconut" });
const [columns, setColumns] = useState([
{
name: "Column 1",
options: {}
},
{
name: "Column with Input",
options: {
customBodyRender: (value, tableMeta, updateValue) => {
return (
<div>
<select value={state.value} onChange={handleChange}>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</div>
);
}
}
}
]);
const options = {
responsive: "scroll"
};
const addColumn = () => {
columns.push({
name: "NewColumn",
label: "NewColumn"
});
setColumns(columns);
};
const data = [["value", "value for input"], ["value", "value for input"]];
return (
<React.Fragment>
<MUIDataTable columns={columns} options={options} data={data} />
//add a new column if this button is clicked
<button onClick={addColumn}>Add Column</button>
</React.Fragment>
);
};
Your new column wasn't actually getting pushed to the columns variable. When you're using useState, you can't make changes to the variable unless you use setColumns, otherwise it won't trigger a rerender. Try this:
const addColumn = () => {
setColumns([ ...columns, {
name: "NewColumn"
}]);
};
Or this:
const addColumn = () => {
const editableColumns = [...columns];
editableColumns.push({
name: "NewColumn"
});
setColumns(editableColumns);
};
Both will work, it's just your preference.
You can test if it's editing the columns with this:
useEffect(() => console.log(columns), [columns]);
Related
I have a dropdown made with react-select, with multiple options that i get from an api. The code is working, when clicked the dropdown show the options, but when I select one option it stop showing the other ones. I don't what could it be since the isMulti prop is on.
Here is the code:
export const DropDown = ({ itemsOption, placeholder }) => {
const [options, setOptions] = useState([]);
const loadOptions = (op) => {
api.get(`${op}`).then(({ data }) => {
setOptions(
data.map((item) => {
return {
key: item.code,
label: item.name_ptbr,
};
})
);
});
};
useEffect(() => {
loadOptions(itemsOption);
}, []);
return (
<>
<DropStyled>
<Select
isMulti
options={options}
name={placeholder}
placeholder={placeholder}
closeMenuOnSelect={false}
/>
</DropStyled>
</>
);
};
An option needs a value property for react-select to map the data to its options.
So when mapping over the fetched data, add value along with label and key.
return {
key: item.code,
label: item.name_ptbr,
value: item.name_ptbr,
};
I have created a page with a multiple state controlled input fields but am having difficulty ensuring that they retain the correct text values on the front end after deleting a row of elements.
Here is an image of the component:
Here is the component which displays the inputs:
<InputContainer>
<PrimaryLabel>Criteria</PrimaryLabel>
{currentRuleCriteria.map((criteria, index) => (
<TripleInputContainer>
<StyledTriplePrimarySelect name="criteria-column" id="criteria-column-select" value={currentRuleCriteria[index].column} onChange={(e) => handleCriteriaColumnChange(index, e.target.value)}>
{columns.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledTriplePrimarySelect>
<StyledTriplePrimarySelect name="criteria-operator" id="criteria-operator-select" value={currentRuleCriteria[index].operator} onChange={(e) => handleCriteriaOperatorChange(index, e.target.value)}>
{criteriaOperators.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledTriplePrimarySelect>
<InputTypeSelectorsContainer>
<StyledColumnIcon active={criteria.valueType === "column"} onClick={() => handleCriteriaValueTypeChange(index, "column")} />
<StyledTextIcon active={criteria.valueType === "text"} onClick={() => handleCriteriaValueTypeChange(index, "text")} />
</InputTypeSelectorsContainer>
{ criteria.valueType === "column" ? (
<StyledPrimarySelect name="criteria-value" id="criteria-value-select" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)}>
{columns.map((option, index) => (
<option key={index} value={option.value}>
{option.text}
</option>
))}
</StyledPrimarySelect>
) : (
<StyledPrimaryInput type="text" placeholder="" value={currentRuleCriteria[index].value} onChange={(e) => handleCriteriaValueChange(index, e.target.value)} />
)}
<RemoveItemContainer onClick={() => handleRemoveCriteria(index)}>
<StyledRemoveItemIcon active={currentRuleCriteria.length > 1} />
</RemoveItemContainer>
</TripleInputContainer>
))}
<AdditionalEntryContainer>
<AdditionalEntryButton onClick={() => handleAddCriteria()}>+ Add another</AdditionalEntryButton>
</AdditionalEntryContainer>
</InputContainer>
Here is the state which is mapped over to produce the input elements:
const [currentRuleCriteria, setCurrentRuleCriteria] = useState([{
column: "title",
operator: "contains",
value: "",
valueType: "text"
}]);
Here are the handler functions for interacting with state:
const handleCriteriaColumnChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].column = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaOperatorChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].operator = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaValueChange = (index, value) => {
setCriteriaValue(value);
const newCriteria = [...currentRuleCriteria];
newCriteria[index].value = value;
setCurrentRuleCriteria(newCriteria);
};
const handleCriteriaValueTypeChange = (index, value) => {
const newCriteria = [...currentRuleCriteria];
newCriteria[index].valueType = value;
setCurrentRuleCriteria(newCriteria);
setCriteriaValueType(value);
};
const handleAddCriteria = () => {
setCurrentRuleCriteria([...currentRuleCriteria, {
column: criteriaColumn,
operator: criteriaOperator,
value: criteriaValue,
valueType: criteriaValueType
}]);
setCriteriaColumn("title");
setCriteriaOperator("contains");
setCriteriaValue("");
setCriteriaValueType("text");
};
const handleRemoveCriteria = (index) => {
const newCriteria = [...currentRuleCriteria];
newCriteria.splice(index, 1);
setCurrentRuleCriteria(newCriteria);
};
The Problem:
I can see that when I add a new criteria a new set of input fields are added. Similarly, when I edit the values the state is always maintained correctly. The issue I am having is when deleting a pre-existing row of elements.
If I have two criteria in place, as in the image above, and I select the first one to be deleted - the state updates correctly, however the incorrect input fields seem to be deleted on the frontend and so it no longer aligns with the state values. In this example the following would then be visible:
I don't understand this as the input fields should be being generated by mapping over the currentRuleCriteria state value.
I have tried many things to try and rectify this, but can't seem to figure it out. I previously did not have the value specified on the individual select elements and was in the same situation as I am currently. Adding in a value field and referencing the state did not change anything and so I believe the select fields are not being populated by state values.
Any thoughts? Thank you!
You should set the key on TripleInputContainer:
<TripleInputContainer key={criteria.id}>
React docs says "Keys should be stable, predictable, and unique". But in your case there may be complete duplicates of the criteria. So it makes sense to use Math.random():
const [currentRuleCriteria, setCurrentRuleCriteria] = useState([
{
id: Math.random(),
column: "title",
operator: "contains",
value: "",
valueType: "text",
},
]);
setCurrentRuleCriteria([
...currentRuleCriteria,
{
id: Math.random(),
column: criteriaColumn,
operator: criteriaOperator,
value: criteriaValue,
valueType: criteriaValueType,
},
]);
It's even better to use nanoid and not worry about collisions.
How can I retrieve the dishId selected from my options react - select that shows me thedishType in order to send them my parent component FormRender and to the backend.
My first dropdown shows me: Menu1, Menu2...
My second one: type2...
So if I click ontype4, how can I store the related dishId(here = 4). I can click on several values i.e: type2 andtype3.
How do I keep the dish ids i.e : 2 and 3 and send them to my FormRender parent
Menus(first page of my multi - step form):
export default function Menus() {
const [selectionMenus, setSelectionMenus] = useState({});
const [selectionDishes, setSelectionDishes] = useState({});
const [menus, setMenus] = useState([])
const [date, setDate] = useState('')
useEffect(() => {
axios
.post(url)
.then((res) => {
console.log(res);
setMenus(res.data.menus);
})
.catch((err) => {
console.log(err);
});
}, []);
const names = menus?.map(item => {
return {
label: item.menuId,
value: item.name
}
})
const types = menus?.flatMap(item => {
return item.dishes.map(d => ({
label: d.dishId,
value: d.dishType
}))
})
const handle = (e) => {
if (e?.target?.id === undefined) return setInfo(e);
if (e?.target?.id === undefined) return setSelectionMenus(e);
if (e?.target?.id === undefined) return setSelectionDishes(e);
switch (e.target.id) {
case "date":
setDate(e.target.value);
break;
...
default:
}
}
};
return (
<>
<form>
<div>My menus</div>
<label>
Dishes :
<Dropdown
options={names}
value={selectionMenus}
setValue={setSelectionMenus}
isMulti={true}
/>
</label>
<label>
<Dropdown
options={types}
value={selectionDishes}
setValue={setSelectionDishes}
isMulti={true}
/>
</label>
<label>
Date:
<div>
<input
type="date"
name='date'
value={date}
onChange={handle}
id="date"
/>
</div>
</label>
...
</form>
<div>
<button onClick={() => nextPage({ selectionDishes, selectionMenus, date })}>Next</button>
</div>
</>
);
}
Here the parent Component FormRender that is supposed to retrieve the values of all dishId selected and send them to the backend:
export default function FormRender() {
const [currentStep, setCurrentStep] = useState(0);
const [info, setInfo] = useState();
const [user, setUser] = useState();
const headers = ["Menus", "Details", "Final"];
const steps = [
<Menus
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>,
<Details
backPage={() => setCurrentStep((s) => s - 1)}
nextPage={setUser}
/>,
<Final />
];
useEffect(() => {
if (info === undefined || user === undefined) return;
const data = {
date: info.date,
id: //list of dishId selected but don't know how to do that??
};
}, [info, user]);
return (
<div>
<div>
<Stepper steps={headers} currentStep={currentStep} />
<div >{steps[currentStep]}</div>
</div>
</div>
);
}
Dropdown:
export default function Dropdown({ value, setValue, style, options, styleSelect, isMulti = false }) {
function change(option) {
setValue(option.value);
}
return (
<div onClick={(e) => e.preventDefault()}>
{value && isMulti === false ? (
<Tag
selected={value}
setSelected={setValue}
styleSelect={styleSelect}
/>
) : (
<Select
value={value}
onChange={change}
options={options}
isMulti={isMulti}
/>
)}
</div>
);
}
Here my json from my api:
{
"menus": [
{
"menuId": 1,
"name": "Menu1",
"Description": "Descritption1",
"dishes": [
{
"dishId": 2,
"dishType": "type2"
},
{
"dishId": 3,
"dishType": "type3"
},
{
"dishId": 4,
"dishType": "type4"
}
]
},
...
]
}
You already store the selected values inside the selectionMenus and selectionDishes states. So, if you want to send them to the parent FormRender component you can instead create those two states inside that component like this:
export default function FormRender() {
const [selectionMenus, setSelectionMenus] = useState();
const [selectionDishes, setSelectionDishes] = useState();
....
}
Then pass those values to the Menus component:
<Menus
selectionMenus={selectionMenus}
setSelectionMenus={setSelectionMenus}
selectionDishes={selectionDishes}
setSelectionDishes={setSelectionDishes}
nextPage={(menu) => {
setInfo(menu);
setCurrentStep((s) => s + 1);
}}
/>
Subsequently, you will have to remove the state from the Menus component and use the one you receive from props:
export default function Menus({ selectionMenus, setSelectionMenus, selectionDishes, setSelectionDishes }) {
/*const [selectionMenus, setSelectionMenus] = useState({}); //remove this line
const [selectionDishes, setSelectionDishes] = useState({});*/ //remove this line
...
}
Finally, you can use inside your useEffect hook the two states and map them to only get the selected ids:
useEffect(() => {
// ... other logic you had
if(selectionDishes?.length && selectionMenus?.length){
const data = {
date: info.date,
id: selectionDishes.map(d => d.dishId),
idMenus: selectionMenus.map(m => m.menuId)
};
}
}, [info, user, selectionMenus, selectionDishes]);
react-select has options to format the component:
getOptionLabel: option => string => used to format the label or how to present the options in the UI,
getOptionValue: option => any => used to tell the component what's the actual value of each option, here you can return just the id
isOptionSelected: option => boolean => used to know what option is currently selected
onChange: option => void => do whatever you want after the input state has changed
value => any => if you customize the above functions you may want to handle manually the value
Hope it helps you
The ReactSelect component in my app is clearing what I have typed after I make a selection. I don't want it to do that.
I want it to leave it as is. When the user has not entered anything, I want it to show the Placeholder text.
I'm using this component as a search box. Selecting an item from the list accomplishes the task. There's no reason to set the component's value to the selection.
react-select does not support autocompletion out of the box so it requires a bit of extra work and hacks to get what you want.
First off, you need to control the inputValue state, some operations like menu-close or set-value will clear the input afterward.
const [input, setInput] = React.useState("");
const handleInputChange = (e, meta) => {
if (meta.action === "input-change") {
setInput(e);
}
};
return (
<Select
value={selected}
onChange={handleChange}
inputValue={input}
isSearchable
{...}
/>
);
react-select also hides the input after an option is selected so you also need to override that behavior as well.
const [selected, setSelected] = React.useState([]);
const handleChange = (s) => {
setSelected({ ...s });
};
React.useLayoutEffect(() => {
const inputEl = document.getElementById("myInput");
if (!inputEl) return;
// prevent input from being hidden after selecting
inputEl.style.opacity = "1";
}, [selected]);
return (
<Select
value={selected}
inputId="myInput"
onChange={handleChange}
{...}
/>
);
Last but not least, you may want to update your input accordingly after a successful selection, here is a basic example, the code below will append the new option value to your current input after a selection.
Also make sure to override the default filterOption to only taking into account the last word when filtering or nothing will match with the options after the first few words.
const [selected, setSelected] = React.useState([]);
const [input, setInput] = React.useState("");
const handleChange = (s) => {
setSelected({ ...s });
setInput((input) => removeLastWord(input) + s.value);
};
const customFilter = () => {
return (config, rawInput) => {
const filter = createFilter(null);
return filter(config, getLastWord(rawInput));
};
};
return (
<Select
value={selected}
filterOption={customFilter()}
onChange={handleChange}
inputValue={input}
components={{
SingleValue: () => null
}}
{...}
/>
);
Where removeLastWord and getLastWord are just some utility functions.
function getLastWord(str: string) {
return str.split(" ").slice(-1).pop();
}
function removeLastWord(str: string) {
var lastWhiteSpaceIndex = str.lastIndexOf(" ");
return str.substring(0, lastWhiteSpaceIndex + 1);
}
Here is a complete example after combining all of the above.
import React from "react";
import Select, { components, createFilter } from "react-select";
import options from "./options";
function getLastWord(str: string) {
return str.split(" ").slice(-1).pop();
}
function removeLastWord(str: string) {
var lastWhiteSpaceIndex = str.lastIndexOf(" ");
return str.substring(0, lastWhiteSpaceIndex + 1);
}
export default function MySelect() {
const [selected, setSelected] = React.useState([]);
const [input, setInput] = React.useState("");
const handleChange = (s) => {
setSelected({ ...s });
setInput((input) => removeLastWord(input) + s.value);
};
const handleInputChange = (e, meta) => {
if (meta.action === "input-change") {
setInput(e);
}
};
React.useLayoutEffect(() => {
const inputEl = document.getElementById("myInput");
if (!inputEl) return;
// prevent input from being hidden after selecting
inputEl.style.opacity = "1";
}, [selected]);
const customFilter = () => {
return (config, rawInput) => {
const filter = createFilter(null);
return filter(config, getLastWord(rawInput));
};
};
return (
<Select
value={selected}
filterOption={customFilter()}
inputId="myInput"
onChange={handleChange}
blurInputOnSelect={false}
inputValue={input}
onInputChange={handleInputChange}
options={options}
isSearchable
hideSelectedOptions={false}
components={{
SingleValue: () => null
}}
/>
);
}
Live Example
You can also override the input styles using the styles props. There is a whole discussion about this issue on this thread
{
input: (provided) => ({
...provided,
input: {
opacity: "1 !important",
},
}),
}
I am having a onChange function i was trying to update the array options by index wise and i had passed the index to the function.
Suppose if i am updating the options array index 0 value so only that value should be update rest should remain as it is.
Demo
Here is what i tried:
import React from "react";
import "./styles.css";
export default function App() {
const x = {
LEVEL: {
Type: "LINEN",
options: [
{
Order: 1,
orderStatus: "INFO",
orderValue: "5"
},
{
Order: 2,
orderStatus: "INPROGRESS",
orderValue: "5"
},
{
Order: 3,
orderStatus: "ACTIVE",
orderValue: "9"
}
],
details: "2020 N/w UA",
OrderType: "Axes"
},
State: "Inprogress"
};
const [postdata, setPostData] = React.useState(x);
const handleOptionInputChange = (event, idx) => {
setPostData({
...postdata,
LEVEL: {
...postdata.LEVEL.options,
[event.target.name]: event.target.value
}
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e, idx)}
/>
);
})}
</div>
);
}
Suppose if i want to add the objects in another useState variable for all the updated options only, will this work?
const posting = {
"optionUpdates": [],
}
const [sentdata , setSentData] = useState(posting);
setSentData({
...sentdata,
optionUpdates: [{
...sentdata.optionUpdates,
displayOrder: event.target.value
}]
})
Basically, you need to spread properly, use callback approach to set state etc.
Change your handler to like this.
Working demo
const handleOptionInputChange = (event, idx) => {
const target = event.target; // with callback approach of state, you can't use event inside callback, so first extract the target from event.
setPostData(prev => ({ // prev state
...prev, // spread prev state
LEVEL: { //update Level object
...prev.LEVEL,
options: prev.LEVEL.options.map((item, id) => { // you need to loop thru options and find the one which you need to update.
if (id === idx) {
return { ...item, [target.name]: target.value }; //spread all values and update only orderStatus
}
return item;
})
}
}));
};
Edit Added some comments to code and providing some explanation.
You were spreading postdata.LEVEL.options for LEVEL which is incorrect. For nested object you need to spread each level.
Apparently, your event.target.name is "orderStatus", so it will add an "orderStatus" key to your postData.
You might want to do something like this:
const handleOptionInputChange = (value, idx) => {
setPostData(oldValue => {
const options = oldValue.LEVEL.options;
options[idx].orderStatus = value;
return {
...oldValue,
LEVEL: {
...oldValue.LEVEL,
options
}
};
});
};
return (
<div className="App">
{postdata.LEVEL.options.map((item, idx) => {
return (
<input
type="text"
name="orderStatus"
value={postdata.LEVEL.options[idx].orderStatus}
onChange={e => handleOptionInputChange(e.target.value, idx)}
/>
);
})}
</div>
);
See this demo