Passing selected option's value to another component in nextjs - javascript

I'm new to nextjs/react so bare with me here. In my project I have multiple select elements with multiple options in each one. When an option is selected or changed I want to pass that value to another component. I was able to pass an onClick event to another component but when I tried a similar solution I wasn't able to get it to work. So the select elements are being mapped in Component A2, but the options for those elements are also being mapped in Component A3 and I need to pass the value to Component B2. You will see in my code I tried to pass it with the "handleOnChange". I'm not very good at explaining things so here is my code snippets, I hope this makes sense:
Parent Component
export default function Post({ globalProps, page, globalPages, sidebarProps }) {
const [addFlexItem, setAddFlexItem] = useState(false)
const [addFlexItemStyles, setFlexItemStyles] = useState()
return (
<Layout globalProps={globalProps}>
<main className={styles.container}>
<FlexSidebar sidebarProps={sidebarProps} onClick={() => setAddFlexItem(true)} handleOnChange={() => setFlexItemStyles()} />
<FlexContainer addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</main>
</Layout>
)
}
Component A1
const FlexSidebar = ({ sidebarProps, onClick, handleOnChange }) => {
return (
<aside className={styles.left_sidebar}>
<section className={styles.wrap}>
{/* we are padding the onClick to the child component */}
{container === true && <FlexSidebarContainer sidebarProps={sidebarProps} onClick={onClick} handleOnChange={handleOnChange} />}
{items === true && <FlexSidebarItems sidebarProps={sidebarProps} />}
</section>
</aside>
)
}
Component A2
const FlexSidebarContainer = ({ sidebarProps, onClick, handleOnChange }) => {
const options = sidebarProps.options
return (
<>
<p className={styles.warning}>{sidebarProps.containerWarningText}</p>
<button type="button" className="btn" onClick={() => onClick()}>
{sidebarProps.addItemBtn}
</button>
<form className={styles.form}>
{options.map((option, index) => {
return (
<div key={index} className={styles.form_item}>
<div className={styles.form_label_wrap}>
<label>{option.title}</label>
</div>
<FlexSidebarSelect options={option.items} handleOnChange={handleOnChange} />
</div>
);
})}
</form>
</>
)
}
Component A3
const FlexSidebarSelect = ({ options, handleOnChange }) => {
return (
<div className={styles.form_item_wrap}>
<select onChange={(value) => handleOnChange(value)}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}
Component B1
const FlexContainer = ({ addFlexItem, addFlexItemStyles }) => {
return (
<section className={styles.right_content}>
<FlexItem addFlexItem={addFlexItem} addFlexItemStyles={addFlexItemStyles} />
</section>
)
}
Component B2
const FlexItem = ({ addFlexItem, addFlexItemStyles }) => {
const [isaddFlexItem, setaddFlexItem] = useState(addFlexItem)
useEffect(() => {
setaddFlexItem(addFlexItem)
}, [addFlexItem])
return (
isaddFlexItem ?
<div className={styles.flex_item}>
<div className={styles.flex_item_wrap}>
<div className={styles.flex_item_inner}>
</div>
<button className={styles.trash}>
</button>
</div>
</div>
: "empty"
)
}
I will add that if I change the code in Component A3 to this, im able to log the value, but I cant get it to work in the parent component.
const FlexSidebarSelect = ({ options, handleOnChange }) => {
const [value, setValue] = useState("")
const handleOptionChange = (e) => {
let value = e.target.value
setValue({
value
})
}
return (
<div className={styles.form_item_wrap}>
<select onChange={handleOptionChange}>
{options.map((item, index) => {
return (
<option key={index} value={item.value}>{item.item}</option>
)
})}
</select>
</div>
)
}

Related

Conditional rendering an input inside a map

I have a list of task which inside have another task(subTask).
I use a map to list all the tasks and have a button to add more subtasks to that task.
The input button is supposed to be invisible unless the button is pressed.
The problem is that I used the useState hook to conditional render the input but whenever I click in any of the button, the inputs of all tasks open and is supposed to only open the input from that certain index.
const Task = () => {
const [openTask, setOpenTask] = useState(false);
return task.map((item, index) => {
return (
<div>
<button onClick={() => setOpenTask(!openTask)}>Add</button>
{item.name}
{openTask && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
Try using array
const Task = () => {
const [openTask, setOpenTask] = useState([]);
const openTaskHandler = (id) => {
setOpenTask([id])
}
return task.map((item, index) => {
return (
<div>
<button onClick={() => openTaskHandler(item.id)}>Add</button>
{item.name}
{openTask.includes(item.id) && <input placeholder="Add more tasks..."/>}
{item.subTask.map((item, index) => {
return (
<>
<span>{item.name}</span>
</>
);
})}
</div>
);
});
};
That's because you are setting openTask to true for all the items. You can create object and store the specific id to true.
for eg.
const [openTask, setOpenTask] = useState({});
and then on button onClick you can set the specific id to open
setOpenTask({...openTask,[`${item.name}-${index}`]:true});
and then check against specific id
{openTask[`${item.name}-${index}`] && <input placeholder="Add more tasks..."/>}

How to return a component in React

i have a button component from material UI. What i wanna do is i click a button and it renders to me a new Button on the screen.
That's what i did
const [button, setButton] = useState([]);
function addButton(){
let newObj = {
button: <Button variant="contained"> A Button</Button>,
};
setButton([...button, newObj])
}
And then in the main function return i did
{button.length !== 0 ? (
button.map((item) => {
<div>{item.button}</div>;
})
) : (
<div>Hello</div>
)}
What am i doing wrong?
When you use a multi-line lambda with braces you need a return...
button.map((item) => {
return (<div>{item.button}</div>);
})
Alternatively you can omit the braces...
button.map((item) => (<div>{item.button}</div>))
This answer tries to address the below problem-statement:
But how could i click one button and render as many buttons as i
clicked?
The below snippet provides a demo of how a new button may be rendered when user clicks an existing button. This expands on the below comment
Creating a new button on clicking an existing button may be
accomplished without needing to hold the component in state.
Please note:
It does not store the button elements into the state
instead, it merely stores the attributes (like the button-label /
text) into the state
Code Snippet
const {useState} = React;
const MyButton = ({
btnLabel, btnName, handleClick, isDisabled, ...props
}) => (
<button
class="myBtn"
onClick={handleClick}
name={btnName}
disabled={isDisabled}
>
{btnLabel}
</button>
);
const MagicButtons = () => {
const [btnText, setBtnText] = useState("");
const [disableBtns, setDisableBtns] = useState(false);
const [myButtons, setMyButtons] = useState(["A Button"]);
const handleClick = () => setDisableBtns(true);
return (
<div>
{
disableBtns && (
<div class="myInput">
<input
value={btnText}
onChange={e => setBtnText(e.target.value)}
/>
<button
onClick={() => {
setMyButtons(prev => ([...prev, btnText]));
setBtnText("");
setDisableBtns(false);
}}
>
Add New Button
</button>
</div>
)
}
{
myButtons.map((txt, idx) => (
<MyButton
handleClick={handleClick}
btnName={idx}
isDisabled={disableBtns ? "disabled" : ""}
btnLabel={txt}
/>
))
}
</div>
);
};
ReactDOM.render(
<div>
DEMO
<MagicButtons />
</div>,
document.getElementById("rd")
);
.myBtn { margin: 15px; }
.myInput > input { margin: 15px; }
<div id="rd" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
You forgot to return the component in the map function
like this
{button.length !== 0 ? (
button.map((item) => {
return (<div>{item.button}</div>);
})
) : (
<div>Hello</div>
)}
the map function with no 'return' keyword must not have the bracket { }
like this
button.map((item) => (<div>{item.button}</div>))

Formik - State and initial values gets overwritten

I want to implement an edit dialog for some metadata that I get from the backend. For this, I'm using Formik. When the user changes one metadata field then an icon is shown, which indicates that the field was changed. On submit, only the updated values should be sent to the backend. I read in other posts that you should compare the current field values with the initial values provided to the Formik form. This works perfectly fine for single values, like a changed title. However, the form I need to implement has also multi-value fields like creators. I implemented a custom field for that where the user can choose/provide multiple values for one field. The values of this field are saved as an array in the Formik form. The problem is that Formik also changes the initialValue of this field to the currently saved array and therefore I'm no longer able to check if the field is updated or not. Furthermore, I save the metadata fields provided by the backend in a state value because the response contains some further information needed for further implementation. This state value also contains the current value (before update) that a metadata field has and is used as initial values for the Formik form. Strangely, the multi-field component not only overwrites the initialValue of the field of the Formik form but also the value of the field in the state which is only read and never directly updated.
My dialog for editing metadata looks as follows:
const EditMetadataEventsModal = ({ close, selectedRows, updateBulkMetadata }) => {
const { t } = useTranslation();
const [selectedEvents, setSelectedEvents] = useState(selectedRows);
const [metadataFields, setMetadataFields] = useState({});
const [fetchedValues, setFetchedValues] = useState(null);
useEffect(() => {
async function fetchData() {
let eventIds = [];
selectedEvents.forEach((event) => eventIds.push(event.id));
// metadata for chosen events is fetched from backend and saved in state
const responseMetadataFields = await fetchEditMetadata(eventIds);
let initialValues = getInitialValues(responseMetadataFields);
setFetchedValues(initialValues);
setMetadataFields(responseMetadataFields);
}
fetchData();
}, []);
const handleSubmit = (values) => {
const response = updateBulkMetadata(metadataFields, values);
close();
};
return (
<>
<div className="modal-animation modal-overlay" />
<section className="modal wizard modal-animation">
<header>
<a className="fa fa-times close-modal" onClick={() => close()} />
<h2>{t('BULK_ACTIONS.EDIT_EVENTS_METADATA.CAPTION')}</h2>
</header>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={currentLanguage.dateLocale}>
<Formik initialValues={fetchedValues} onSubmit={(values) => handleSubmit(values)}>
{(formik) => (
<>
<div className="modal-content">
<div className="modal-body">
<div className="full-col">
<div className="obj header-description">
<span>{t('EDIT.DESCRIPTION')}</span>
</div>
<div className="obj tbl-details">
<header>
<span>{t('EDIT.TABLE.CAPTION')}</span>
</header>
<div className="obj-container">
<table className="main-tbl">
<tbody>
{metadataFields.mergedMetadata.map(
(metadata, key) =>
!metadata.readOnly && (
<tr key={key} className={cn({ info: metadata.differentValues })}>
<td>
<span>{t(metadata.label)}</span>
{metadata.required && <i className="required">*</i>}
</td>
<td className="editable ng-isolated-scope">
{/* Render single value or multi value input */}
{console.log('field value')}
{console.log(fetchedValues[metadata.id])}
{metadata.type === 'mixed_text' &&
!!metadata.collection &&
metadata.collection.length !== 0 ? (
<Field name={metadata.id} fieldInfo={metadata} component={RenderMultiField} />
) : (
<Field
name={metadata.id}
metadataField={metadata}
showCheck
component={RenderField}
/>
)}
</td>
</tr>
),
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{/* Buttons for cancel and submit */}
<footer>
<button
type="submit"
onClick={() => formik.handleSubmit()}
disabled={!(formik.dirty && formik.isValid)}
className={cn('submit', {
active: formik.dirty && formik.isValid,
inactive: !(formik.dirty && formik.isValid),
})}
>
{t('UPDATE')}
</button>
<button onClick={() => close()} className="cancel">
{t('CLOSE')}
</button>
</footer>
<div className="btm-spacer" />
</>
)}
</Formik>
</MuiPickersUtilsProvider>
</section>
</>
);
};
const getInitialValues = (metadataFields) => {
// Transform metadata fields provided by backend
let initialValues = {};
metadataFields.mergedMetadata.forEach((field) => {
initialValues[field.id] = field.value;
});
return initialValues;
};
Here is RenderMultiField:
const childRef = React.createRef();
const RenderMultiField = ({ fieldInfo, field, form }) => {
// Indicator if currently edit mode is activated
const [editMode, setEditMode] = useState(false);
// Temporary storage for value user currently types in
const [inputValue, setInputValue] = useState('');
useEffect(() => {
// Handle click outside the field and leave edit mode
const handleClickOutside = (e) => {
if (childRef.current && !childRef.current.contains(e.target)) {
setEditMode(false);
}
};
// Focus current field
if (childRef && childRef.current && editMode === true) {
childRef.current.focus();
}
// Adding event listener for detecting click outside
window.addEventListener('mousedown', handleClickOutside);
return () => {
window.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Handle change of value user currently types in
const handleChange = (e) => {
const itemValue = e.target.value;
setInputValue(itemValue);
};
const handleKeyDown = (event) => {
// Check if pressed key is Enter
if (event.keyCode === 13 && inputValue !== '') {
event.preventDefault();
// add input to formik field value if not already added
if (!field.value.find((e) => e === inputValue)) {
field.value[field.value.length] = inputValue;
form.setFieldValue(field.name, field.value);
}
// reset inputValue
setInputValue('');
}
};
// Remove item/value from inserted field values
const removeItem = (key) => {
field.value.splice(key, 1);
form.setFieldValue(field.name, field.value);
};
return (
// Render editable field for multiple values depending on type of metadata field
editMode ? (
<>
{fieldInfo.type === 'mixed_text' && !!fieldInfo.collection && (
<EditMultiSelect
collection={fieldInfo.collection}
field={field}
setEditMode={setEditMode}
inputValue={inputValue}
removeItem={removeItem}
handleChange={handleChange}
handleKeyDown={handleKeyDown}
/>
)}
</>
) : (
<ShowValue setEditMode={setEditMode} field={field} form={form} />
)
);
};
// Renders multi select
const EditMultiSelect = ({ collection, setEditMode, handleKeyDown, handleChange, inputValue, removeItem, field }) => {
const { t } = useTranslation();
return (
<>
<div ref={childRef}>
<div onBlur={() => setEditMode(false)}>
<input
type="text"
name={field.name}
value={inputValue}
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => handleChange(e)}
placeholder={t('EDITABLE.MULTI.PLACEHOLDER')}
list="data-list"
/>
{/* Display possible options for values as dropdown */}
<datalist id="data-list">
{collection.map((item, key) => (
<option key={key}>{item.value}</option>
))}
</datalist>
</div>
{/* Render blue label for all values already in field array */}
{field.value instanceof Array &&
field.value.length !== 0 &&
field.value.map((item, key) => (
<span className="ng-multi-value" key={key}>
{item}
<a onClick={() => removeItem(key)}>
<i className="fa fa-times" />
</a>
</span>
))}
</div>
</>
);
};
// Shows the values of the array in non-edit mode
const ShowValue = ({ setEditMode, form: { initialValues }, field }) => {
return (
<div onClick={() => setEditMode(true)}>
{field.value instanceof Array && field.value.length !== 0 ? (
<ul>
{field.value.map((item, key) => (
<li key={key}>
<span>{item}</span>
</li>
))}
</ul>
) : (
<span className="editable preserve-newlines">{''}</span>
)}
<i className="edit fa fa-pencil-square" />
<i className={cn('saved fa fa-check', { active: initialValues[field.name] !== field.value })} />
</div>
);
};
export default RenderMultiField;
This is initialValues before a change and after:
InitialValues before change
InitialValues after change
This is the state of MetadataFields and FetchedValues before and after change:
State before change
State after change

Rendering components based on prop type

I have a small restaurant app which lets users order from a menu. The menu has three types: food, drink, dessert. The component structure from top to bottom is Order -> Menu -> MenuItem. I want to separate the menu page based on type (example: all food items are under a title called FOOD and so on). Right now, the Menu component receives the menu array as a prop from Order and renders MenuItem for each item in the array. Each item has a property called type. I omitted certain parts of the code unrelated to this issue for brevity.
//Order
export default function Order() {
const [menu, setMenu] = useState<Array<{}>>([]);
const [total, setTotal] = useState(0);
useEffect(() => {
apiFetch("menu").then((json) => setMenu(json.menu));
}, []);
async function handleSubmit(e: any) {
e.preventDefault();
const selectedItems = getSelectedItems(TotalStore);
apiFetch("order", "post", { selectedItems })
.then((json) => {
alert("Order has been submitted");
setTotal(0);
TotalStore.reset();
localStorage.setItem("last_order_id", json.order.id);
function checkOrderStatus() {
apiFetch(
`order/${json.order.id || localStorage.getItem("last_order_id")}`
).then((placedOrder) => {
const { order } = placedOrder;
if (order[0].status === 2) {
alert("Your order is ready!");
} else {
setTimeout(checkOrderStatus, 5000);
}
});
}
checkOrderStatus();
})
.catch((error) => {
alert("Server error");
});
}
function orderPlaced(total: number) {
return total !== 0 ? true : false;
}
return (
<div>
{menu.length > 0 ? (
<>
<div className="menu">
<div className="menu-title">Food Menu</div>
<form id="menu-form" onSubmit={handleSubmit} autoComplete="off">
<Menu onChange={itemChanged} props={menu} />
<button type="submit" disabled={!orderPlaced(total)}>
Place Order
</button>
</form>
</div>
<div className="order-total">
<h2>
Total: $<span>{total.toFixed(2)}</span>
</h2>
</div>
</>
) : (
<>Loading Menu</>
)}
</div>
);
}
//Menu
export default function Menu({ onChange, props }: MenuProps) {
return (
<div>
{props.map((food: any, index: number) => {
return (
<MenuItem
key={index}
onChange={onChange}
type={food.type}
item={food}
/>
);
})}
</div>
);
}
//MenuItem
export default function MenuItem({ onChange, item, type }: MenuItemProps) {
return (
<div>
<article className="menu-item" data-item-type={type}>
<h3 className="item-name">{item.name}</h3>
<input
type="number"
className="menu-item-count"
min="0"
value={data.count}
onChange={menuItemCountChange}
/>
<strong className="item-price">${item.price.toFixed(2)}</strong>
</article>
</div>
);
}
Here is what the page currently looks like:
You want to group your menu data by the food.type property. One way would be to sort the menu items into their food type "category, then rendering each group separately.
export default function Menu({ onChange, items }) {
const foodCategories = items.reduce((categories, item) => {
if (!categories[item.type]) {
categories[item.type] = []; // <-- new array for category type
}
categories[item.type].push(item); // <-- push item into category type
return categories;
}, {});
return (
<div>
{Object.entries(foodCategories).map(([type, foodItems]) => (
<div key={type}>
<h1>{type}</h1> // <-- category header
{foodItems.map((food, index) => ( // <-- map food items
<MenuItem
key={index}
onChange={onChange}
type={food.type}
item={food}
/>
))}
<div>
))}
</div>
);
}

Is it possible to display updated state from useState in another component?

I have this component which uses useState const [answers, setAnswerFunction] = useState(options);
Once the answers state has been updated in this component I would like to use the updated state and display it in another component. Is this possible?
I had a look at a similar question which says to use useContext I have not looked into this yet as I have never used it (Is it possible to share states between components using the useState() hook in React?) but I wondered if there would be a simpler way?
Code:
const QuestionBox = ({
question,
options,
correct,
incrementScore,
incrementResponse,
}) => {
const [response, setResponse] = useState("");
const [answers, setAnswerFunction] = useState(options);
const computeAnswer = answer => {
if (answer === correct) {
setResponse("correct");
incrementScore();
incrementResponse();
} else {
setResponse("sorry wrong!");
incrementResponse();
}
};
return (
<div className="questionBox">
<div className="question"> {question} </div>
{answers.map((answer, index) => {
return (
<button
key={index}
className="answerBtn"
type="button"
onClick={() => {
setAnswerFunction([answer]);
computeAnswer(answer);
}}
>
{answer}
</button>
);
})}
{response === "correct" ? (
<div className="correctResponse"> {response} </div>
) : (
<div className="wrongResponse"> {response} </div>
)}
</div>
);
};
export default QuestionBox;
I want to display the state from the component abover answers here on Result card via the prop userAnswer:
const ResultCard = ({
score,
getQuestions,
qbank,
userAnswer
}) => {
return (
<div>
<div>You scored {score} out of 5! </div>
<div className="playBtnBox">
<button className="playBtn" type="button" onClick={getQuestions}>
Play again
</button>
</div>
<div>
{qbank.map((questionObject) => {
return (
<div>
<div className="questionBox"> {questionObject.question}</div>
<div className="resultCardCorrect"> Correct Answer: {questionObject.correct}</div>
</div>
);
})}
</div>
<div className="resultCardCorrect"> Your Answer: {userAnswer}</div>
</div>
);
};
export default ResultCard;

Categories