How do I sync tests in React? - javascript

I have CommentsList component which displays a list with all the comments. Each comment has a 'Reply to' button that opens the AddComment component (this component allows me to add a reply to a comment). To display the AddComment component for each comment, I used an array of states.
The AddComment component contains a text area for the input, a cancel button and a submit button. When I click on the submit button and the reply is added successfully, the AddComment component closes. If the input is empty and I click on the submit button, the component doesn't close because the input can't be empty in order to be submitted successfully.
I want to test this functionality so that I can verify if the AddComment component disappears after I post a reply.
The problem is that in tests my AddComment component doesn't disappear when I click on the submit button. What I noticed is that the comment is added succesfully, but the state of the AddComment component for the comment isn't changed. When I click on submit button the input is submitted, but the function that changes the state is never called. I think the problem might be the fact that the actions don't synchronize.
I tried to use await act to render CommentsList component to make sure that the test run closer to how React works in the browser, but my AddComment component still doesn't disappear.
Here's my CommentsList component
function CommentsList(props) {
const { t } = useTranslation();
const [hasReplyCommentBox, setHasReplyCommentBox] = useState([]);
function toggleHasReplyComment(commentIndex) {
var auxState = { ...hasReplyCommentBox };
auxState[commentIndex] = auxState[commentIndex] ? 0 : 1;
setHasReplyCommentBox(auxState);
}
function replyToCommentButton(commentIndex) {
return [
<span
id={"replyButton-" + commentIndex}
onClick={() => toggleHasReplyComment(commentIndex)}>
{t('Reply to')}
</span>
];
}
function commentReplyBox(commentIndex, parentCommentId) {
return hasReplyCommentBox[commentIndex]
?
<AddComment
id={props.codeId}
parentCommentId={parentCommentId}
commentIndex={commentIndex}
toggleHasReplyComment={toggleHasReplyComment}
updateComments={props.updateComments}
/>
:
null
}
return (
<Layout>
<Layout>
<List
itemLayout="horizontal"
dataSource={props.comments}
renderItem={(comment, commentIndex) => (
<List.Item>
<CommentCard
userId={comment.user_id}
datePosted={comment.date_posted}
body={comment.body}
actions={replyToCommentButton(commentIndex)}
children={commentReplyBox(commentIndex, comment.id)}
/>
</List.Item>
)}
/>
<AddComment
id={props.codeId}
updateComments={props.updateComments}
/>
</Layout>
</Layout>
);
}
Here's my AddComment component
function AddComment(props) {
const { t } = useTranslation();
const { TextArea } = Input;
const [form] = Form.useForm();
const [comment, setComment] = useState();
const [onCommentAddSuccess, setOnCommentAddSuccess] = useState(0);
const buttonStyle = {
float: 'right'
};
function onCommentChange(newComment) {
setComment(newComment.target.value);
}
function updateOnCommentAddSuccess(onCommentAddSuccess) {
setOnCommentAddSuccess(onCommentAddSuccess + 1);
}
function resetCommentInput() {
setComment('');
}
function onFormReset() {
form.resetFields();
}
function toggleHasReplyCommentOnPost(parentCommentId, commentIndex) {
if (parentCommentId !== undefined) {
console.log('comentariu adaugat cu succes');
props.toggleHasReplyComment(commentIndex);
}
}
function submitComment() {
let request = {
body: comment,
code_id: props.id,
parent_comment_id: props.parentCommentId
};
fetch('/api/comment/add',
{
method: 'POST',
body: JSON.stringify(request)
}
).then(response => response.json())
.then(data => {
if (data.success === 1) {
updateOnCommentAddSuccess(onCommentAddSuccess);
props.updateComments(onCommentAddSuccess);
resetCommentInput();
toggleHasReplyCommentOnPost(props.parentCommentId, props.commentIndex);
}
});
}
return (
<>
<Form form={form} name="comment" className="comment-form"
onFinish={submitComment}>
<Form.Item name="body" label={t('Comment')}>
<TextArea placeholder={t('Leave a comment')}
onChange={onCommentChange}
id={"parent-comment-" + props.parentCommentId} />
</Form.Item>
<Form.Item style={buttonStyle}>
<Space>
{props.parentCommentId
?
<Button id={"cancelAddReplyComment-" + props.parentCommentId}
type="secondary" className = "comment-form-button"
onClick={
() => props.toggleHasReplyComment(props.commentIndex)
}>
{t('Cancel')}
</Button>
:
null
}
<Button type="primary" htmlType="submit"
className = "comment-form-button"
id={"post-comment-button-" + props.parentCommentId}
onClick={onFormReset}>
{t('Post')}
</Button>
</Space>
</Form.Item>
</Form>
</>
);
}
And here's how my test looks like
test ('Toggle displaying add reply to comments', async () => {
const comments = [
{
id: 'ID-1',
user_id: 'USER-ID-1',
date_posted: '2020-01-01 01:00:00',
body: 'First comment'
}
];
await act(async () => {
Promise.resolve(render(
<CommentsList comments={comments} />, container
));
});
// Open AddComment component
const replyButton = container.querySelector("#replyButton-0");
await fireEvent.click(replyButton);
// Insert input in the text area
const userInput = container.querySelector("#parent-comment-ID-1");
await userEvent.type((userInput), 'reply');
// Submit the input
const postButton = container.querySelector("#post-comment-button-ID-1");
await fireEvent.click(postButton);
// Check if the AddComment component is closed
expect(container.querySelector("#cancelAddReplyComment-ID-1")).toBeFalsy();
});

Related

Handling the Check Box filter in React Js

Im Having a Table which has multiple records and Filter component with the Search Bar. What im trying to do is Based on the value selected by the user from all the filters i have pass those arrays to parent and form an object,
Im having 3 components here,
1)Parent : Data
export default function Data(props) {
const [domain, setDomain] = useState([]);
const [fileType, setFileType] = useState([]);
const [entity, setEntity] = useState(["Patents"]);
const [year, setYear] = useState({});
//This is the search bar state
const [keywords, setKeywords] = useState([]);
//based on the filter values im calling the API to get the records for table based on the value selected by the user from my filer
useEffect(() => {
const fetchResults = async (projectid) => {
const url = props.apiURL.rpaapiurl + "/search";
console.log("fetchData called-->" + url);
const resultsObj = {
projectId: projectid,
filter: {
domain: domain,
fileType: fileType,
entity: entity,
},
};
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(resultsObj),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log("All data-->", data);
setResults(data);
};
fetchResults(5);
}, [domain, fileType, entity]);
const handleFileType = (fileTypeArray) => {
setFileType(fileTypeArray);
};
return (
<Item1>
<Dropdown onChangeFileType={(FileTypeFilteredArray) => handleFileType(FileTypeFilteredArray)} ></Dropdown>
</Item1>
<Item2>
<Table
Data={dataresults}
Attributes={resultTable}
entitytypeHandler={props.entitytypeHandler}
></Table>
</Item2>
)
From the data parent component im passing the hadler which will return updated array from the child and im setting it to state.
2)Child : Dropdown
export default function Dropdown(props) {
return (
<FilterItem>
<Input
type="search"
placeholder="Search in title, description, keywords"
></Input>
<Filter1></Filter1>
<Filetr2></Filetr2>
<ContentFormat
onChangeFileType={props.onChangeFileType}
></ContentFormat>
<Filter4></Filter4>
<Filter5></Filter5>
<TextWrap>
<P text="End year" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</TextWrap>
</FilterItem>
)}
Nothing special here since we can not skip a component passing the same thing to nested child,
Nested Child : ContentFormat
export default function ContentFormat(props) {
const [isDisplay, setIsDisplay] = useState("false");
const array = ["HTML", "PDF"];
const toggle = () => {
setIsDisplay(!isDisplay);
};
let fileTypeArray = [];
const handleSelection = (event) => {
const value = event.target.value;
console.log("value-->", +value);
if (event.target.checked == true) {
fileTypeArray.push(value);
console.log("if fileTypeArray-->", fileTypeArray);
} else if (fileTypeArray.length > 0) {
fileTypeArray = fileTypeArray.filter((element) => {
console.log("element-->", +element);
if (event.target.value !== element) return element;
});
console.log("else fileTypeArray-->", fileTypeArray);
}
console.log("function fileTypeArray-->", fileTypeArray);
};
const applyClickHandler = () => {
console.log("Applied fileTypeArray-->", fileTypeArray);
props.onChangeFileType(fileTypeArray);
};
return (
<div>
<DropContent>
<DropButton onClick={toggle}>
{" "}
<P text="By Content Format" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</DropButton>
<ContextWrapper style={{ display: isDisplay ? "none" : "block" }}>
<P
text="Filter by Extension types"
fontSize="18px"
color="#ACACAC"
textAlign="center"
padding="22px 32px 14px"
></P>
<DropScroll className="sl-style-3">
{array.map((item, index) => {
return (
<ContextItem key={index}>
<DropList
onHandleSelection={handleSelection}
text={item}
value={item}
></DropList>
</ContextItem>
);
})}
</DropScroll>
<ApplyButton onClick={applyClickHandler}>
<P text="Apply" fontSize="16px" color="#fff" textAlign="center"></P>
</ApplyButton>
</ContextWrapper>
</DropContent>
</div>
);
}
4)DropList
export default function DropList(props) {
const changeHandler = (e) => {
console.log(e);
props.onHandleSelection(e);
};
return (
<div>
<div className="">
<TickBox
type="checkbox"
id={props.id}
name={props.name}
value={props.value}
onChange={(e) => {
changeHandler(e);
}}
/>
{props.text}
</div>
</div>
);
}
I'm getting the updated array on click of apply button in the parent but if user un-selects any check box the it deleting the complete array
In data i have to form the object base on the state array passed by all the filters, i tried for the one filter as above but its not working can any one suggest better way to do it,
Because here handling one filter is default and i have to do it for total 5 filters
So any suggestion or one common component for all the filters
Im not sure whether i should be asking these kinda questions or not since I'm very at posting the right questios but pardon me if its wrong question or the way of asking is wrong,
Any help would be appricited.

React: [Label]-[input]-[edit btn]-[save btn-(hidden)]

As the title states, Ive got a simple crud operation I could use some help with the useEffect/useState hook in how to implement.
I've got the input set to disabled by default, displaying the previous value via the placeholder. The state is then updated via a useState/useEffect hooks. Also, the second button (save) is set to hidden by default.
Goal:
essentially, just trying to setup an event listener for each edit-input button: hide the edit button, enable the input, unhide the save button.
with a separate (2nd) event listener on the save button: to hide the save button, unhide the edit button, return the input to disabled, turn the placeholder to the new value, and submit the value(I've got a good idea of the last part)
JSX:
<label className="col">
<div className="row">
<p className="bold nowrap inline-value-title" >Test Name: </p>
<input
id="display-name-change"
type="text"
className="form-reset inline-input"
onChange={(e) => setDisplayName(e.target.value)}
value={displayName}
placeholder={user.displayName}
disabled
/>
<button type="button" className="inline-button edit-val-btn">
<FontAwesomeIcon icon={faPenToSquare} />
</button>
<button type="button" className="hidden inline-button save-val-btn">.
<FontAwesomeIcon icon={faCircleCheck} />
</button>
</div>
</label>
My Javascript: (as you can probably tell it's still very Vanilla and I think that's the problem)...
const editValueBtns = document.querySelectorAll('.edit-val-btn');
const saveValueBtns = document.querySelectorAll('.save-val-btn');
useEffect(() => {
editValueBtns.forEach((button) => {
button.addEventListener('click', (e) => {
button.classList.add('hidden')
button.nextSibling.classList.remove('hidden')
button.parentElement.children[1].removeAttr("disabled") ;
})
})
saveValueBtns.forEach((button) => {
button.addEventListener('click', (e) => {
button.classList.add('hidden')
button.previousSibling.classList.remove('hidden')
button.parentElement.children[1].addAttr("disabled") ;
})
})
}, []);
EDIT: Showing the inputs being submitted to Firebase/Firestore
const handleSubmit = async (e) => {
e.preventDefault();
let selectedFile = document.querySelector('#thumbnailInput')
// displayname
if(displayName.length == 0){console.log('No change of name')}
else {
console.log('change to displayname')
updateProfile(user, { displayName })
setDoc(doc(db, 'users', user.uid), { displayName }, { merge: true })
}
// phone Number
if (phoneNo.length == 0){console.log('No change of phone no')}
else {
console.log('change to phone')
updateProfile(user, { phoneNo })
setDoc(doc(db, 'users', user.uid), { phoneNo }, { merge: true })
}
// title
if (title.length == 0){console.log('No change of title')}
else {
console.log('change to title')
updateProfile(user, { title })
setDoc(doc(db, 'users', user.uid), { title }, { merge: true })
}
// avatar thumbnail
if(selectedFile.files[0] == undefined){
console.log('no change to thumbnail')
} else {
console.log('change to thumbnail')
// pass the path in ref to create a StorageReference
const storageRef = ref(storage,`thumbnails/${user.uid}/${displayName}`) //thumbnail.name
// upload image, file is a blob here
await uploadBytes(storageRef, thumbnail);
const downloadUrl = await getDownloadURL(storageRef);
// this function returns promise too, add await
await updateProfile(user, { photoURL: downloadUrl })
updateProfile(user, { photoURL: downloadUrl})
setDoc(doc(db, 'users', user.uid), {
photoURL: downloadUrl,
}, { merge: true })
}
// clear all form inputs
const inputs = e.target.querySelectorAll('.form-reset')
inputs.forEach((input) => {
input.value=""
})
}
I see your vanilla js way, and raise you the react way. In react, you shouldn't have to use document.querySelector, previousSibling, parentElement, classList.add, classList.remove, addAttr or button.addEventListener. See solution in CodeSandbox or below:
App.jsx
import { Row } from "./components/Row";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Row placeholder="input 1" />
<Row placeholder="input 2" />
<Row placeholder="input 3" />
</div>
);
}
Row.jsx
import { useState } from "react";
export const Row = ({ defaultValue, placeholder }) => {
const [value, setValue] = useState(defaultValue);
const [disabled, setDisabled] = useState(true);
const handleEditClick = () => {
setDisabled(false);
};
const handleSaveClick = () => {
setDisabled(true);
// save logic goes here
};
return (
<label className="col">
<div className="row">
<p className="bold nowrap inline-value-title">Test Name:</p>
<input
type="text"
className="form-reset inline-input"
onChange={(e) => {
setValue(e.target.value);
}}
value={value}
placeholder={placeholder}
disabled={disabled}
/>
{disabled && (
<button
type="button"
onClick={handleEditClick}
className="inline-button edit-val-btn"
>
edit
</button>
)}
{!disabled && (
<button
type="button"
onClick={handleSaveClick}
className="hidden inline-button save-val-btn"
>
save
</button>
)}
</div>
</label>
);
};
small tweaks.. it appears that logging the console kept it from doing what it did. also removed the e from the parenthesis after each 'click'. the addAttr and removeAttr also were replaced... the rest of the functionality can be placed in either of the listeners..
EDIT: added a 2nd input and it appears to only work in the 1st input....
...yes I'm talking to myself.
EDIT 2: It worked fine until the page refreshed... removed the dependency array for it to work every time. I feel like i still need a cleanup function, but I can't just place the event listeners into a function can i? Really, if you're reading this i would love some more experienced input... :)
useEffect(() => {
editValueBtns.forEach((button) => {
button.addEventListener('click', () => {
button.classList.add('hidden')
button.nextSibling.classList.remove('hidden')
button.parentElement.children[1].disabled = false ;
})
})
saveValueBtns.forEach((button) => {
button.addEventListener('click', () => {
button.classList.add('hidden')
button.previousSibling.classList.remove('hidden')
button.parentElement.children[1].disabled = true ;
})
})
});

Cannot read property of undefined how to work around this in the DOM?

I execute a component and this component fills the value profilePicRef once. However, I only want to display the Upload button when profilePicRef.current.preview is also no longer zero. However, I always get the error message TypeError: Cannot read property 'preview' of undefined. My question is, how can I now say if it is undefined, then don't take it into account and if it is not zero show it.
<PhotoFileHandler ref={profilePicRef} />
{
profilePicRef.current.preview !== null &&
<button className="button is-primary is-outlined" type="butto"
onClick={() => { onClickUpload(profilePicRef);
setActiveModal(''); }}>
<i className="fas fa-file-image"></i> Upload</button>
}
PhotoFileHandler
import React, { useState, forwardRef, useImperativeHandle, } from "react";
function PhotoFileHandler(props, ref) {
const [picName, setPicName] = useState(null);
const [preview, setPreview] = useState(null);
const [isPreview, setIsPreview] = useState(true);
const fileSelectedHandler = (event) => {
....
setPicName(event.target.files[0].name);
setPreview(reader.result);
setIsPreview(true);
}
}
catch (err) {
}
};
useImperativeHandle(ref, () => ({
isPreview,
preview,
picName,
checkProfilPicture() {
if (!preview) {
setIsPreview(false);
return false;
}
else {
setIsPreview(true);
return true;
}
},
getImage() {
return preview
},
removePreview() {
setIsPreview(false)
setPreview(null);
setPicName(null);
}
}),
);
return (
<div>
<input class="file-input" type="file" name="resume" accept=".png,.jpg,.jpeg, .jfif"
onChange={fileSelectedHandler} />
</div>
);
};
// eslint-disable-next-line
PhotoFileHandler = forwardRef(PhotoFileHandler);
export default PhotoFileHandler;
Is important to do with ref?
Alternative 0: Without Ref, Parent State
Alternative with state, you can see how it works here: https://codesandbox.io/s/admiring-gates-ctw6m?file=/src/App.js) :
Include one variable "file" const [selectedFile, setSelectedFile] = React.useState(null)
Send the setter function to PhotoFileHandler, I did something like: <PhotoFileHandler onImage={setSelectedFile} />
In PhotoFileHandler I did:
const fileSelectedHandler = (event) => {
props.onImage(event.target.files[0]);
}
Alternative 1: Force update with Parent State
If you need the ref, one workaround can be the trigger it when change it, like: https://codesandbox.io/s/sleepy-butterfly-iqmw3?file=/src/App.js,
Define in your parent component one state: const [wasUpdated, setWasUpdated] = React.useState("");
Include this in your validation to show the preview and upload button:
profilePicRef.current &&
profilePicRef.current.preview !== null &&
wasUpdated && (
<AllHtml />
)
)
Send the setter to the children <PhotoFileHandler ref={profilePicRef} onChange={setWasUpdated} />
In the children component you can trigger the event after updating preview.
useEffect(() => {
props.onChange(preview);
}, [preview]);

How to select data that i want when click button (React JS)

i have this example data, when i click button show more it will show popup (using Modal reactbootstrap) and i will show more detail like ID,Name,Age,City,Number,Address,Education and many more.. how i can select and get all data in popup only when i click button 'show more'
and this my code
import React from "react";
import MUIDataTable from "mui-datatables";
import axios from "axios";
import { Modal, Button } from "react-bootstrap";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
errors: null,
isLoading: true,
};
}
get = async () => {
const option = {
url: "/api/url",
method: 'POST',
headers: {
"Access-Control-Allow-Origin": "*"
},
data: {
"data": {
"data": "........."
},
"encrypt": 0
}
};
axios(option)
.then(response => {
const tableData = response.data.data.map(post => {
const {ID,Name,Age,City,Number,Address,Education} = post;
const Popup = () => {
const [lgShow, setLgShow] = React.useState(false);
const [isOpen, setIsOpen] = React.useState(false);
const showModal = () => {
setIsOpen(true);
};
const hideModal1 = () => {
setIsOpen1(false);
};
return (
<div>
<Button onClick={() => setLgShow(true)}>Show more</Button>
<Modal
size="lg"
show={lgShow}
onHide={() => setLgShow(false)}
aria-labelledby="example-modal-sizes-title-lg"
>
<Modal.Header closeButton class="modal-dialog modal-lg">
<Modal.Title id="example-modal-sizes-title-lg">
Data {nama_loket}
</Modal.Title>
</Modal.Header>
<Modal.Body>
Detail<br/>
<div><b> ID</b></div><br />
<div>{ID}</div><br />
<div><b>Name</b></div><br />
<div >{Name}</div><br />
<div><b>Age</b></div><br />
<div>{Age}</div><br />
<div><b>City</b></div><br />
<div>{City}</div><br />
<div><b>Number</b></div><br />
<div>{Number}</div><br />
<div><b>Adress</b></div><br />
<div>{Address}</div><br />
<div><b>Educaton</b></div><br />
<div>{Education}</div><br />
</Modal.Body>
</Modal>
</div>
);
};
return [
[ID],
[Name],
[Age],
[City],
[Number],
<Popup></Popup>
];
});
this.setState({
data: tableData,
isLoading: false
});
console.log(response.data.data);
console.log(this.state.data)
})
// If we catch any errors connecting, let's update accordingly
.catch(error => {
console.log(error.response);
this.setState({ error, isLoading: false })
}
);
}
componentDidMount() {
this.get();
}
render() {
const { isLoading} = this.state;
const columns = ["ID", "Name", "Age", "City", "Phone Number",""];
const options = {
filterType: "dropdown",
responsive: "scroll",
selectableRows:false,
};
return (
<div>
{!isLoading ? (
<MUIDataTable
data={this.state.data}
columns={columns}
options={options}
/>)
: (
<p>Loading...</p>
)}
</div>
);
}
}
export default App
how i get data in my popup when i click. example i have 5 row, when i click second row, data will selected and get is only the second data in second row.. can anyone help me?
You can do this by saving the row clicked in the state and then using the state to show it on the modal, just like you are doing to show your modal.
<Button
onClick={() => {
setLgShow(true)
setSelectedPost(post)
}}
>
Show more
</Button>
The idea is to have one button for each post, but you don't need to render the Modal more than once, so render the Modal outside of the response.data.data.map and use the state saved on the selectedPost to show the data inside the Modal.
I think it's better to create the table manually using css https://www.w3schools.com/css/css_table.asp
And for the rows just use React mapping https://reactjs.org/docs/lists-and-keys.html
Then create a button inside your mapping that call a function to open the modal.
Create a new state called displayedData to store the row that you want to display. Inside your render:
{data.map((value, index) => {
<tr key={index}>
<td>{value.id}</td>
<td>{value.name}</td>
. . .
<td>{value.phone}</td>
<td> <button onClick={()=> this.OpenData(value)}>Show More</button> </td>
</tr>
})}
and for the OpenData function:
async OpenData(value){
await this.setState({displayedData : value})
this.openModal()
}
Last, just use displayedData state to display your data inside the modal.
edit:
Also, move your modal from your axios fetch, just create its own function. use your axios fetch just to update your data state

ReactJs: Prevent multiple times button press

In my React component I have a button meant to send some data over AJAX when clicked. I need to happen only the first time, i.e. to disable the button after its first use.
How I'm trying to do this:
var UploadArea = React.createClass({
getInitialState() {
return {
showUploadButton: true
};
},
disableUploadButton(callback) {
this.setState({ showUploadButton: false }, callback);
},
// This was simpler before I started trying everything I could think of
onClickUploadFile() {
if (!this.state.showUploadButton) {
return;
}
this.disableUploadButton(function() {
$.ajax({
[...]
});
});
},
render() {
var uploadButton;
if (this.state.showUploadButton) {
uploadButton = (
<button onClick={this.onClickUploadFile}>Send</button>
);
}
return (
<div>
{uploadButton}
</div>
);
}
});
What I think happens is the state variable showUploadButton not being updated right away, which the React docs says is expected.
How could I enforce the button to get disabled or go away altogether the instant it's being clicked?
The solution is to check the state immediately upon entry to the handler. React guarantees that setState inside interactive events (such as click) is flushed at browser event boundary. Ref: https://github.com/facebook/react/issues/11171#issuecomment-357945371
// In constructor
this.state = {
disabled : false
};
// Handler for on click
handleClick = (event) => {
if (this.state.disabled) {
return;
}
this.setState({disabled: true});
// Send
}
// In render
<button onClick={this.handleClick} disabled={this.state.disabled} ...>
{this.state.disabled ? 'Sending...' : 'Send'}
<button>
What you could do is make the button disabled after is clicked and leave it in the page (not clickable element).
To achieve this you have to add a ref to the button element
<button ref="btn" onClick={this.onClickUploadFile}>Send</button>
and then on the onClickUploadFile function disable the button
this.refs.btn.setAttribute("disabled", "disabled");
You can then style the disabled button accordingly to give some feedback to the user with
.btn:disabled{ /* styles go here */}
If needed make sure to reenable it with
this.refs.btn.removeAttribute("disabled");
Update: the preferred way of handling refs in React is with a function and not a string.
<button
ref={btn => { this.btn = btn; }}
onClick={this.onClickUploadFile}
>Send</button>
this.btn.setAttribute("disabled", "disabled");
this.btn.removeAttribute("disabled");
Update: Using react hooks
import {useRef} from 'react';
let btnRef = useRef();
const onBtnClick = e => {
if(btnRef.current){
btnRef.current.setAttribute("disabled", "disabled");
}
}
<button ref={btnRef} onClick={onBtnClick}>Send</button>
here is a small example using the code you provided
https://jsfiddle.net/69z2wepo/30824/
Tested as working one: http://codepen.io/zvona/pen/KVbVPQ
class UploadArea extends React.Component {
constructor(props) {
super(props)
this.state = {
isButtonDisabled: false
}
}
uploadFile() {
// first set the isButtonDisabled to true
this.setState({
isButtonDisabled: true
});
// then do your thing
}
render() {
return (
<button
type='submit'
onClick={() => this.uploadFile()}
disabled={this.state.isButtonDisabled}>
Upload
</button>
)
}
}
ReactDOM.render(<UploadArea />, document.body);
You can try using React Hooks to set the Component State.
import React, { useState } from 'react';
const Button = () => {
const [double, setDouble] = useState(false);
return (
<button
disabled={double}
onClick={() => {
// doSomething();
setDouble(true);
}}
/>
);
};
export default Button;
Make sure you are using ^16.7.0-alpha.x version or later of react and react-dom.
Hope this helps you!
If you want, just prevent to submit.
How about using lodash.js debounce
Grouping a sudden burst of events (like keystrokes) into a single one.
https://lodash.com/docs/4.17.11#debounce
<Button accessible={true}
onPress={_.debounce(async () => {
await this.props._selectUserTickets(this.props._accountId)
}, 1000)}
></Button>
If you disable the button during onClick, you basically get this. A clean way of doing this would be:
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
export default function CalmButton(props) {
const [executing, setExecuting] = useState(false);
const {
disabled,
onClick,
...otherProps
} = props;
const onRealClick = async (event) => {
setExecuting(true);
try {
await onClick();
} finally {
setExecuting(false);
}
};
return (
<Button
onClick={onRealClick}
disabled={executing || disabled}
{...otherProps}
/>
)
}
See it in action here: https://codesandbox.io/s/extended-button-that-disabled-itself-during-onclick-execution-mg6z8
We basically extend the Button component with the extra behaviour of being disabled during onClick execution. Steps to do this:
Create local state to capture if we are executing
Extract properties we tamper with (disabled, onClick)
Extend onClick operation with setting the execution state
Render the button with our overridden onClick, and extended disabled
NOTE: You should ensure that the original onClick operation is async aka it is returning a Promise.
By using event.target , you can disabled the clicked button.
Use arrow function when you create and call the function onClick. Don't forget to pass the event in parameter.
See my codePen
Here is the code:
class Buttons extends React.Component{
constructor(props){
super(props)
this.buttons = ['A','B','C','D']
}
disableOnclick = (e) =>{
e.target.disabled = true
}
render(){
return(
<div>
{this.buttons.map((btn,index) => (
<button type='button'
key={index}
onClick={(e)=>this.disableOnclick(e)}
>{btn}</button>
))}
</div>
)}
}
ReactDOM.render(<Buttons />, document.body);
const once = (f, g) => {
let done = false;
return (...args) => {
if (!done) {
done = true;
f(...args);
} else {
g(...args);
}
};
};
const exampleMethod = () => console.log("exampleMethod executed for the first time");
const errorMethod = () => console.log("exampleMethod can be executed only once")
let onlyOnce = once(exampleMethod, errorMethod);
onlyOnce();
onlyOnce();
output
exampleMethod executed for the first time
exampleMethod can be executed only once
You can get the element reference in the onClick callback and setAttribute from there, eg:
<Button
onClick={(e) => {
e.target.setAttribute("disabled", true);
this.handler();
}}
>
Submit
</Button>
Keep it simple and inline:
<button type="submit"
onClick={event => event.currentTarget.disabled = true}>
save
</button>
But! This will also disable the button, when the form calidation failed! So you will not be able to re-submit.
In this case a setter is better.
This fix this set the disabled in the onSubmit of the form:
// state variable if the form is currently submitting
const [submitting, setSubmitting] = useState(false);
// ...
return (
<form onSubmit={e => {
setSubmitting(true); // create a method to modify the element
}}>
<SubmitButton showLoading={submitting}>save</SubmitButton>
</form>
);
And the button would look like this:
import {ReactComponent as IconCog} from '../../img/icon/cog.svg';
import {useEffect, useRef} from "react";
export const SubmitButton = ({children, showLoading}) => {
const submitButton = useRef();
useEffect(() => {
if (showLoading) {
submitButton.current.disabled = true;
} else {
submitButton.current.removeAttribute("disabled");
}
}, [showLoading]);
return (
<button type="submit"
ref={submitButton}>
<main>
<span>{children}</span>
</main>
</button>
);
};
Another approach could be like so:
<button onClick={this.handleClick} disabled={isLoading ? "disabled" :""}>Send</button>
My approach is if event on processing do not execute anything.
class UploadArea extends React.Component {
constructor(props) {
super(props)
this.state = {
onProcess:false
}
}
uploadFile() {
if (!this.state.onProcess){
this.setState({
onProcess: true
});
// then do your thing
this.setState({
onProcess: false;
});
}
}
render() {
return (
<button
type='submit'
onClick={() => this.uploadFile()}>
Upload
</button>
)
}
}
ReactDOM.render(<UploadArea />, document.body);
Try with this code:
class Form extends React.Component {
constructor() {
this.state = {
disabled: false,
};
}
handleClick() {
this.setState({
disabled: true,
});
if (this.state.disabled) {
return;
}
setTimeout(() => this.setState({ disabled: false }), 2000);
}
render() {
return (
<button type="submit" onClick={() => this.handleClick()} disabled={this.state.disabled}>
Submit
</button>
);
}
}
ReactDOM.render(<Form />, document.getElementById('root'));

Categories