How to handle two onClicks in the same component in React? - javascript

There are two custom popups with OK and Cancel.
When you click the OK button, a review is written, and it is a code that is sent to the server as a post request. Then, the confirmation button appears once more, and a pop-up indicating that the writing is complete is displayed to the user. My problem here is that the post request is sent, but I don't get a second popup stating that the writing is complete.
The picture above is a pop-up design I made.
This code is the parent component.
export async function getServerSideProps(context) {
const token = cookie.parse(context.req.headers.cookie).accessToken;
const reviewId = context.query.reviewId;
const viewLecture = await getViewLecture(reviewId, token);
return {
props: { token, reviewId, viewLecture },
};
}
const WriteMentee = ({ token, reviewId, viewLecture }) => {
const [reviewInfo, setReviewInfo] = useState([]);
const [modal, setModal] = useState(false);
const [confirm, setConfirm] = useState(false);
return (
<>
<section className={styles.contentSection}>
{modal ? (
<ModalWithBackground
setModal={setModal}
className={styles.modalHeight}
>
<ReviewModal
mainText={"후기 등록"}
subText={"작성한 후기를 등록하시겠습니까?"}
cancelBtn={setModal}
confirmBtn={async () => {
const res = await writeReviewAPI(
token,
reviewId,
content,
score
);
if (res == 200 || res == 201) {
console.log("200 || 201");
setConfirm(!confirm);
}
}}
/>
{confirm ? (
<ConfirmModal
mainText={"후기 등록"}
subText={`후기 등록이 완료되었습니다.\n수강하시느라 고생 많으셨습니다.`}
confirm={() => {
router.push("/mentee/mypage/menteeReview");
}}
/>
) : (
<></>
)}
</ModalWithBackground>
) : (
<></>
)}
</section>
</>
)
👇 This component is the component of the first popup image.
const ReviewModal = ({ mainText, subText, cancelBtn, setConfirm }) => {
return (
<section>
<article className={styles.roundModal}>
<h3 className={styles.title}>{mainText}</h3>
<p className={styles.subTitle}>{subText}</p>
<div className={styles.btnSection}>
<button className={styles.cancel} onClick={cancelBtn}>
<span>취소</span>
</button>
<button className={styles.check} onClick={() => setConfirm(false)}>
<span>확인</span>
</button>
</div>
</article>
</section>
);
};
👇 This component is the component of the second popup image.
const ConfirmModal = ({ mainText, subText, confirm }) => {
return (
<section>
<article className={styles.roundModal}>
<h3 className={styles.title}>{mainText}</h3>
<p className={styles.subTitle}>{subText}</p>
<div className={styles.confirmSection}>
<button className={styles.confirm} onClick={confirm}>
<span>확인</span>
</button>
</div>
</article>
</section>
);
};

I think you can use your confirm state to handle which content is showing.
<ModalWithBackground
setModal={setModal}
className={styles.modalHeight}
>
{confirm ? (
<ConfirmModal
mainText={"후기 등록"}
subText={`후기 등록이 완료되었습니다.\n수강하시느라 고생 많으셨습니다.`}
confirm={() => {
router.push("/mentee/mypage/menteeReview");
}}
/>
) : (
<ReviewModal
mainText={"후기 등록"}
subText={"작성한 후기를 등록하시겠습니까?"}
cancelBtn={setModal}
confirmBtn={async () => {
const res = await writeReviewAPI(
token,
reviewId,
content,
score
);
if (res == 200 || res == 201) {
console.log("200 || 201");
setConfirm(!confirm);
}
}}
/>
)}
</ModalWithBackground>

On your parent component, you are passing a confirmBtn prop, which you should be calling in the onClick on the first modal, and doesn't take any arguments. That should handle the opening of the other modal.
But in the ReviewModal, you call a different function called setConfirm and pass it false. You should just be calling confirmBtn (although the name might not matter if you're not using TS). But no need to pass it an argument since that's all handled by the parent component.
Just pass the onClick handler the method itself and change the props to take in confirmBtn instead of setConfirm.
<button className={styles.check} onClick={confirmBtn}>
<span>확인</span>
</button>
And then also double check the api call is returning 200 just in case!

I solved it by writing code like this 😄
{modal ? (
<ModalWithBackground
setModal={setModal}
className={styles.modalHeight}
>
<ReviewModal
mainText={"후기 등록"}
subText={"작성한 후기를 등록하시겠습니까?"}
cancelBtn={() => {
setModal(false);
}}
confirmBtn={async () => {
const res = await writeReviewAPI(
token,
reviewId,
content,
score
);
if (res == 200 || res == 201) {
console.log("200 || 201");
setConfirm(!confirm);
}
}}
/>
</ModalWithBackground>
) : (
<></>
)}
{confirm ? (
<ModalWithBackground
setModal={setModal}
className={styles.modalHeight}
>
<ConfirmModal
mainText={"후기 등록"}
subText={`후기 등록이 완료되었습니다.\n수강하시느라 고생 많으셨습니다.`}
confirm={() => {
router.push("/mentee/mypage/menteeReview");
}}
/>
</ModalWithBackground>
) : (
<></>
)}

Related

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

How to dynamically change className of a component in React?

I want to make a button check which add a new calssName to my list. I use a function to update a state and take the string. If you want to help me be more specific because I am a beginner. Thanks !
const [check, setCheck] = useState({
id: '',
status: false
});
This is the function. With 'on' I take the string to add to id.
let complet = (on) =>{
if(check.status == false){
setCheck({
id: on,
status: true
})
}
else {
setCheck({
id: on,
status: false
})
}
}
And how I Display the list and how I check correspond to the string.
return(
<div className='display'>
{ list.map( (list,index) => (
<div className={ check.status && check.id == list ? 'lista complet' : 'lista'} key= {index} id='lista' >
{list}
<button className='btnCheck' onClick={complet.bind(this, list)}> <FcCheckmark/> </button>
<button className='btnRemove' onClick={remove.bind(null, list)}> <BsTrash/> </button>
</div>
))}
</div>
)
If you want to store the checked ids and the unchecked ids, you must change your state variable because currently it can only stores a single element. However, it seems you are rendering a list of elements that can be checked individually
Here is a possible solution :
function App({list}) {
const [checkIds, setCheckIds] = useState(() => {
const item = localStorage.getItem('checkIds');
return item ? JSON.parse(item) : {};
});
// reset the checkIds when the list is updated
useEffect(() => setCheckIds({}), [list]);
// save the checkIds into the local storage
useEffect(() => {
localStorage.setItem('checkIds', JSON.stringify(checkIds));
}, [checkIds]);
function checkId(id) {
setCheckIds({...checkIds, [id]: true);
}
function uncheckId(id) {
setCheckIds({...checkIds, [id]: false);
}
return (
<div className='display'>
{list.map(id => (
<div key={id} id={id} className={check[id] ? 'lista complet' : 'lista'}>
{id}
<button className='btnCheck' onClick={() => checkId(id)}>
<FcCheckmark/>
</button>
<button className='btnRemove' onClick={() => uncheckId(id)}>
<BsTrash/>
</button>
</div>
))}
</div>
)
}

Render a button (when hovered) for only one element of an array, when mapping an array

const Chat = () => {
const bodyRef = useRef(null)
const [{messages,roomMessages,roomID,socket,user}, dispatch] = useStateValue()
const [dropdown, setDropdown] = useState(false)
useEffect(()=>{
const setScreen = () =>{
bodyRef.current.scrollTop = bodyRef.current.scrollHeight
}
setScreen()
},[])
const updateMessages = (message) =>{
const allMessages = messages
allMessages.push(message)
dispatch({
type: "SET_MESSAGES",
item: allMessages
})
bodyRef.current.scrollTop = bodyRef.current.scrollHeight
}
useEffect(() => {
socket.on('new-message', (message) =>{
if(messages === null){
console.log('here it was null')
} else {
updateMessages(message)
}
})
// clean up socket on unmount
}, [])
const handleDropdown = (index) =>{
setDropdown(true)
console.log(index)
if(dropdown === true){
setDropdown(false)
}
}
return (
<div className={'chat'}>
<ChatHeader/>
<div ref={bodyRef} className={'chat__body'}>
{messages?.map((item,index)=>(
<Emojify key={item._id}>
<div
onMouseEnter={() => handleDropdown(index)}
onMouseLeave={handleDropdown}
className={`chat__message ${item.name === user.displayName && "chat__reciever"}`}>
<h5 className={'chat__messageUser'}>
{item.name}
</h5>
<p className={'chat__messagetext'}>
{item.message}
</p>
{dropdown && <Button><ArrowDropDownIcon/></Button>}
{item.img && <img alt={''} className={'chat__img'} src={item.imgURL}/>}
</div>
</Emojify>
))}
</div>
<div className={'chat__footer'}>
<ChatFooter/>
</div>
</div>
);
I want to render the Button {dropdown && <Button><ArrowDropDownIcon/></Button>} when it is hovered over.
Right now if I hover over one div it gets rendered for all other mapped divs but I don't want this; it shoul be for a specific div only.
How would I do this? Does someone have an idea? I passed index but can't seem to use it as I desire.
Instead of storing a single "global" boolean for dropdown open or not (which all mapped elements are reading), you could correlate a specific dropdown you want to be open. It's good you are already passed an index to the handler.
Change state to store null (no dropdown) or an index (show dropdown).
const [dropdown, setDropdown] = useState(null);
Update the handler to store/toggle an index.
const handleDropdown = (index) =>{
setDropDown(value => value === index ? null : index);
}
In the render simply check if the current index matches the dropdown state.
<div
onMouseEnter={() => handleDropdown(index)}
onMouseLeave={() => handleDropdown(index)} // <-- handle index in mouse leave
className={`chat__message ${
item.name === user.displayName && "chat__reciever"
}`}
>
<h5 className={"chat__messageUser"}>{item.name}</h5>
<p className={"chat__messagetext"}>{item.message}</p>
{dropdown === index && ( // <-- check index match with dropdown value
<Button>
<ArrowDropDownIcon />
</Button>
)}
{item.img && (
<img alt={""} className={"chat__img"} src={item.imgURL} />
)}
</div>

How to run loader icon on deleted row rendered from a list in react?

Hi i am rendering a list of photos from api response. When i delete on one of them, only that row should show the loader. But as of now i see the loader icon on all rows
let boilerImages;
if( listOfPhotos.data!== undefined && listOfPhotos.data.task && listOfPhotos.data.task.metadata !==''){
boilerImages =
listOfPhotos.data &&
JSON.parse(listOfPhotos.data.task.metadata).files.map((token, index) => {
console.log('key', index)
const fileUrl = `url/${token}?subscription-key="something"`
return (
<div className="list-wrapper" key={token+index}>
<div className="img-block">
<img className="img-block-bucket" src={fileUrl}/>
/* This is the part of loader - start */
{
this.props.isDeleting ?
<SvgIcon
svg={"icons/loading-spinner.svg"}
key={index}
fromUrl={true}
className={"deleting-svg"}
/>:
<label className="img-block-file-name">
{this.getFileName(fileUrl)}
</label>
}
/* This is the part of loader - end */
</div>
<button disabled={this.props.isDeleting} className="del-block" onClick={() => removePhoto(token)}>
<SvgIcon
svg={"icons/ic_trash_bin_48_black.svg"}
key={index}
fromUrl={true}
className={!this.props.isDeleting ? "del-block-svg" : "del-block--disabled"}
/>
</button>
</div>
)});
}
this.props.isDeleting is set to true in redux state, when i call removePhoto action and this.props.isDeleting is set to false in redux state, when i call renderPhoto action on update
Instead of isDeleting being a boolean, it will need to be an array of booleans where each entry is the loading state of the list item, then you just need to do something like
{(this.props.isDeleting[index] && ) && (
<SvgIcon
svg={"icons/loading-spinner.svg"}
key={index}
fromUrl={true}
className={"deleting-svg"}
/>)}
Thanks to #Bill. So instead of setting true or false at redux store. I tried having an object. So i used the token passed in my action - removePhoto
When i called removePhoto action
this.props.isDeleting: {
...state.data,
key: action.payload, //key: token
status: true
}
When i called renderPhoto action
this.props.isDeleting: {
...state.data,
key: null,
status: false
}
so the component has code something like this:
let boilerImages;
if( listOfPhotos.data!== undefined && listOfPhotos.data.task && listOfPhotos.data.task.metadata !==''){
boilerImages =
listOfPhotos.data &&
JSON.parse(listOfPhotos.data.task.metadata).files.map((token, index) => {
const fileUrl = `url/${token}?subscription-key="something"`
return (
<div className="list-wrapper" key={index}>
<div className="img-block">
<img className="img-block-bucket" src={fileUrl} onError={() => this.updateCRM(token)}/>
/* This is the part of loader - start */
{
this.props.isDeleting.key === token ?
<SvgIcon
svg={"icons/loading-spinner.svg"}
key={index}
fromUrl={true}
className={"delete-loader"}
/> :
<label className="img-block-file-name">
{this.getFileName(fileUrl)}
</label>
}
/* This is the part of loader - end */
</div>
<button disabled={this.props.isDeleting.status} className="del-block" onClick={() => removePhoto(token)}>
<SvgIcon
svg={"icons/ic_trash_bin_48_black.svg"}
key={index}
fromUrl={true}
className={!this.props.isDeleting.status ? "del-block--enabled" : "del-block--disabled"}
/>
</button>
</div>
)});
}

Categories