Rendering a new component by adding data to Formik values object - javascript

I have a speed dial's icon that, when clicked, should add an accordion item to the accordion list.
Its onClick attribute calls a handleClick function that currently adds an object to formik.values.
The object is added to the values map as expected but the user needs to refresh the screen in order to see the new accordion item.
I guess I'm not properly handling the change of state in the form.
I know formik exposes the handleChange method but I'm not sure if and how it should be used in this scenario.
function AddSpeedDial({formik}) {
const dialActions = [
{
icon: <DinnerDiningIcon />,
name: 'New Dish',
handleClick: () => {
const dishCardsLength = Object.keys(formik.values.dishCards).length;
formik.values.dishCards[dishCardsLength+1] = {
dishName: ''
}
}
}
];
return (
<Box>
<SpeedDial>
{dialActions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
onClick={action.handleClick}
/>
))}
</SpeedDial>
</Box>
);
}

Related

How can I resolve a single reference in react?

fruit json value.
console.log(fruit)
[
{
content: "apple",
information: [
{
calorie: "122"
},
],
},
{
content: "banana",
information: [
{
calorie: "221"
},
],
},
{
content: "mango",
information: [
{
calorie: "52"
},
],
},
];
Create a button by running the map function on the information on the fruit.
The dynamically created button is {value.calorie} first button 122 second button 221 third button 52 is created.
And the content also maps function to fruit and puts {fruit.content} as the argument value in the button.
And set useRef so that when the value.calorie button is clicked, the fruit content button is also clicked.
const Content = useRef(null);
const Calorie = useRef(null);
const onClickContent = (content) => {
console.log(content);
};
const onClickCalorie = (calorie) => {
Content.current.click();
console.log(calorie);
};
return (
<>
fruit.map((Fields, index) => (
<>
<p>{Fields.content}</p>
<button style={{"display": "none"}} ref={Content} onClick={() => onClickContent(Fields.content)}></button>
</>
{Fields.information.map((Value, key) => {
return (
<>
<p>{Value.calorie}</p>
<button onClick={() => onClickCalorie(Value.calorie)}>{Value.calorie}</button>
</>
)
})}
))
</>
)
The problem is that when you click the 122 displayed on the button, the content is also output to the console, apple should be output, but mango is output.
If you click the button with calorie 221, banan should be output, but mango is output to the console. This is said to be because of a single reference.
How can I solve the single reference function? I do not know.
To me it looks like you can remove the ref, as all that it's currently being used for is to trigger a click event on your first (content) button when you click on your second (calorie) buttons. If that's the case, you can instead call onClickContent() manually inside of onClickCalorie(). First, you can update your onClickCalorie() to pass through the data that onClickContent() needs:
<button onClick={() => onClickCalorie(Value.calorie, Fields.content)}>{Value.calorie}</button>
Then, in your onClickCalorie(), update the parameters, and call onClickContent() manually to fire the same function you would trigger if you clicked your Content button:
const onClickCalorie = (calorie, content) => {
onClickContent(content);
// ... do stuff specific to clicking on the calorie button ...
console.log(calorie);
};
This way, you don't need to use any refs, as you don't need to explicitly refer to any DOM elements.
If the only thing that your onClickCalorie() function does is call/trigger onClickContent(), then you can remove the onClickCalorie() function and call onClickContent() directly:
<button onClick={() => onClickContent(Fields.content)}>{Value.calorie}</button>
Otherwise, if you really need to use refs, you can follow this approach where you can create a ref for each of your buttons:
const contentBtns = useRef([]);
and then within a useEffect() update the array size if the fruit array changes:
useEffect(() => {
contentBtns.current = contentBtns.current.slice(0, fruit.length);
}, [fruit]);
and then populate the array stored in contentBtns.current so that each index stores the button element:
<button
style={{"display": "none"}}
ref={el => contentBtns.current[index] = el}
onClick={() => onClickContent(Fields.content)}>
</button>
Now when you call onClickCalorie, you can refer to a specific button to click based on the index. You will need to pass through the index into your onClickCalorie() to have access to it:
const onClickCalorie = (calorie, index) => {
contentBtns.current[index].click();
console.log(calorie);
};

how to only display the information with matched id inside map method

I found myself stupid and could not get my head around with the logic.
I would like to show a few info triggered byonClick, and only those with matched id.
For example, if I click on the button with id of 1, it would only want to show values in that specific object with id:1 like description, library, etc. Right now, all the data are displayed, and because I am using component in material ui, every drawer component are displayed on top of each other (overlapping).
I know the reason causing this is because I have the drawer component inside the map method, but what could be potential solution?
Below are my simple code,
The structure of my data looks like this,
export const projectdata = [
{
id: 1,
title: "",
subtitle: "",
thumbnail: "",
description:
"",
tech: [
],
library: [""],
WebsiteUrl: "",
GitHubUrl: "",
},
...more data with sequential id number...
]
Original:
const handleDrawerOpen = (event) => () => {
setOpen(!open);
};
...
...
<SwipeableDrawer
open={open}
onClose={handleDrawerOpen(false)}
onOpen={handleDrawerOpen(true)}>
***...only show data with matched id...***
</SwipeableDrawer>
I have already map the array to display the data on webpage inside a div like this,
<div>
{ projectdata?.map(({ id, title, subtitle, thumbnail, description, tech, WebsiteUrl, GitHubUrl, library, index }) => (
<>
<Card>
...some info displayed here...
<button onClick={handleDrawerOpen(id)}></button>
</Card>
<SwipeableDrawer>
***...only show data with matched id...***
</SwipeableDrawer>
</>
))}
<div>
One solution I can think of:
with useState(), pass in id as a prop
const [projectDetailId, setProjectDetailId] = useState(null);
const [projectDetailPage, setProjectDetailPage] = useState(false);
const handleDrawerOpen = (id) => {
setProjectDetailId(id);
setProjectDetailPage(true);
};
const handleDrawerClose = () => {
setProjectDetailId(null);
setProjectDetailPage(false);
};
...
...
{projectDetailId === id ?
<SwipeableDrawer
open={projectDetailPage}
onClose={handleDrawerClose}
></SwipeableDrawer>
: null
}
However, this will trigger strange behavior of the drawer (lagging and no animation), especially with mobile device.
Possibly due to this logic projectDetailId === id ? true : false.
Ok, after your update, your problem is that you create multiple drawers, one for each id. When you click on open and set the open prop to true, all your drawers use this same prop so they all open.
You should move the Drawer out of the for and only create one, and send your object that has the id as a prop to the content of the drawer you have.
something like:
const handleDrawerOpen = (yourData) => () => {
setOpen(!open);
setYourData(yourData)
};
...
// and somewhere on your code
<SwipeableDrawer open={open}>
<SomeComponentToShowTheData data={yourData}/>
</SwipeableDrawer>

Checkbox state gets empty after page change

I have an array of objects that looks like this:
const columns = [
{
key: "Source_campname",
title: "TS Camp Name",
customElement: function (row) {
return (
<FormControlLabel
control={
<Checkbox
checked={checkbox[row.id]}
key={row.id}
onChange={() =>
handleChange(row.Source_campname, row.id, checkbox)
}
name={row.id}
/>
}
label={[row.Source_campname]}
/>
);
}
},
{
key: "Tracker_campname",
title: "TR Camp Name"
}
];
You can see a "handleChange" function above, this is used to check/uncheck the component
The handleChange function looks like this:
const handleChange = (name, campid) => {
setCheckBox({ ...checkbox, [campid]: !checkbox[campid] });
};
You can also see a "customElement" function above. This function is rendered in another React component named ThanosTable. I will just write down part of the code where the rendering of customElement happens below.
return (
<> columnArray[0].customElement(row) </>
);
In the end you get 10 checkboxes, and you have a few pages that can be changed using pagination.
Do check my codesandbox link here for a working example:
https://codesandbox.io/s/magical-germain-8tclq
Now I have two problems:
Problem 1) If I select a few checkboxes, then go to second page and return, the checkbox state is empty and the original checkboxes are unselected. No idea why that is happening. How do I prevent that?
Problem 2) The value of checkbox state is always an empty object ({}) inside customElement function. You can see this by checking console.log(checkbox) inside customElement function (Check Line 76 in codesandbox). I thought it should be an array with selected checkbox items.
The useEffect hook embodies all the lifecycle events of a component. Therefore if you try to set checkbox in useEffect it'll infinitely update the component because updating state calls useEffect. This is probably why you see your state constantly being reset.
Instead, initialize your state with the rows before rendering.
const rows = [
...
];
let checkboxObj = {};
// if (rows) {
rows.forEach((e) => {
checkboxObj[e.id] = false;
});
const [checkbox, setCheckBox] = useState(checkboxObj);

How to remove an index from array in react on specific click

I'm building a quiz/survey builder, kind of CMS interface which will allow users to add as many questions as they want by clicking the type of question they want.
To start with, my state is set up in the App component as follow:
state = {
components: API.components,
comps: []
}
The admin screen has a selected number of buttons which will activate a question onClick. The question comes from the API.components.
For example, we have:
- Welcome Message
- Thank You Message
- Yes or No
/* 1. Generate a list of buttons with onClick props.addQuestion passing a fixed id via 'i' parameter */
const QuestionTypes = props => {
return (
<div data-name='typequestion'>
{props.details.map((el, i) => <div key={i}><button className="sm" onClick={() => props.addQuestion(i)}>{el.label}</button></div>)}
</div>
)
}
See Menu Questions screenshot: https://www.awesomescreenshot.com/image/3982311/8964261115690780c3f67d390ce08665
onClick, each of these buttons will trigger the 'addQuestion' method, which will pass a fixed ID (key) to the 'selectComponent' function to add the selected component to the comps[] array:
/* onClick, a method 'addQuestion' is called */
/* This method will setState and call a function selectComponent passing a key (id) in order to select the correct component */
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
The selectComponent function has a switch to pass the correct component:
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
This will add an element to comps[] array:
[
0: {element here ..}
1: {element here ..} etc.
]
Here an example of the code for the component:
const ThankYouMessage = props => (
<section className="ui-component-view" data-name="thankyou">
<img src={ThankYouImage} alt="x" />
<h3>Thanks for completing our form!</h3>
<div>
<DeleteButton deleteQuestion={props.deleteQuestion} />
</div>
</section>
);
See Selected Welcome Message Component screenshot: https://www.awesomescreenshot.com/image/3982315/f59e1bf79a31194aa3ee3ad2467658a0
PROBLEM:
As you can see, each component will have a delete button.
While each component is added to the array without issues, I can't find a way to delete ONLY the selected component when I click the delete button.
I've tried to use .filter(), splice() but I don't have the right index for the newly created or updated array list.
I want to use the React way to do it, not jQuery or Javascript-ish.
Example of Delete Button. Please note that the props.index is passing the original clicked button id (key), which will not match the newly comps[] array index:
const DeleteButton = props => (
<span className="deleteButton" onClick={() => props.deleteQuestion(props.index)}>×<small>Delete</small></span>
);
export default DeleteButton;
Here the Delete method:
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the clicked button index for that component
// 3. Update state
this.setState({ comps });
}
Please see the full code for the App component:
class App extends React.Component {
state = {
components: API.components,
comps: [],
multichoice: {}
}
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the component related to the delete button
// 3. Update state
this.setState({ comps });
}
render() {
return (
<Container>
<Row>
<Col>
<h1>Survey Builder </h1>
</Col>
</Row>
<Row>
<Col lg={3} className="border-right">
<QuestionTypes addQuestion={this.addQuestion} details={this.state.components} />
</Col>
<Col lg={9}>
<Row>
<Col lg={12}>
<QuestionEdit comps={this.state.comps} details={this.state.components} />
</Col>
</Row>
</Col>
</Row>
</Container>
)
}
}
export default App;
You should not keep the components inside the state (cause that breaks the components lifecycle and it is hard to compare them). Instead, just keep the keys:
addQuestion = key => {
this.setState({
comps: [...this.state.comps, key]
});
}
Then inside render(), map the keys to the components:
{this.state.comps.map((key, index) => <SomeComponent remove={() => this.removeQuestion(index)} />)}
Now removeQuestion can simply be:
removeQuestion(index) {
this.setState(({ comps }) => ({ comps: comps.filter((_, i) => i !== index) }));
}
EDITED:
No components should be held on the state, just objects representing the questions.
The comps state should be immutable, that means that each time a question is added or deleted you should create a new Array out of the old one, currently on the state.
Since you're not using any advanced state managers (like Redux, etc.), and nor should you at this point, I would suggest having a data atribute on each question with the question ID on it. Once clicked you can fetch the question ID from the target the click event is carrying and use it to figure out where is the question item reside on the comps state. Once you have it, create a new comps state, by constructing a new Array which does not have that question you've just deleted.
I would also like to recommend not using a switch/case here, since it defies the open/close principle. I think you will find a dictionary approach, where you map the type of the question to the corresponding component, much more scaleable and maintainable.
Hope this helps :)

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories