How can I show another Popconfirm inside another Popconfirm?
The function handleDelete it return true or false. I can see on console the log message but i cannot see the second popconfirm.
render: (text, record) =>
<Popconfirm title="Sure to delete?" onConfirm={() => {
if(this.handleDelete(id)){
console.log("it show true")
return (
<Popconfirm title="Force Delete?" onConfirm={() => {
this.handleInside()
}}>
</Popconfirm>
)
}
}}>
<Button type="link"><b>Delete</b></Button>
</Popconfirm>
}
onConfirm appears to be a event handler when you confirm the action on your Popconfirm, so it doesn't make sense to return JSX inside that method and assume that it will somehow magically render it on the screen.
What you can do is setting the state of the force delete status and render it based on that state.
Something like this:
const [forceDelete, setForceDelete] = useState(false);
...
<Popconfirm title="Sure to delete?" onConfirm={() => {
if (this.handleDelete(id)){
setForceDelete(true);
....
{forceDelete && <Popconfirm ... onConfirm={() => { ... setForceDelete(false); }} />}
...
Related
I'm working on a simple todos app and I'm stuck at a point. Basically, my array filtering is not working. It's not doing anything in fact and I couldn't find out why. I'm using Material UI in the app and I'm suspecting there is something related to that but couldn't figure out entirely.
I'm trying to delete one todo by clicking the trash icon which triggers "deleteTodo" function. But it's not deleting it from the todos. Actually, as I said it's doing nothing. I'm keeeping my todos in the localStorage.
Here is my delete one todo function:
function deleteTodo(id) {
setTodos(todos.filter((todo,i,arr) => {
console.log("id:", id)
console.log("todo.id:",todo.id)
console.log("are equal:", todo.id === id)
console.log(i, arr)
return (todo.id !== id)
}))
}
console output:
[Log] id: – "37dbcd88d5a"
[Log] todo.id: – "37dbcd88d5a"
[Log] are equal: – true
[Log] 1 – [{text: "two", done: false, id: "7dbcd88d5a3"}, {text: "one", done: false, id: "37dbcd88d5a"}] (2)
my component as a whole:
import { uid } from 'uid'
import { useState , useEffect, useLayoutEffect, useRef } from "react"
import { Card, CardContent, Modal, List, ListItem, Box, Button, IconButton, TextField, Typography } from "#mui/material"
import styles from "../styles/Todos.module.css"
import { TransitionGroup } from 'react-transition-group';
import ReportProblemIcon from '#mui/icons-material/ReportProblem';
import DeleteIcon from '#mui/icons-material/Delete';
import EditIcon from '#mui/icons-material/Edit';
export default function Todos () {
const [ text, setText ] = useState("")
const [ todos, setTodos ] = useState([])
const [ showClearTodosModal, setShowClearTodosModal] = useState(false)
const inputRef = useRef()
useEffect(() => {
// localStorage.todos && console.log('b1:',JSON.parse(localStorage.todos))
if (localStorage.todos) {
// localStorage.todos && console.log(JSON.parse(localStorage.todos))
setTodos(JSON.parse(localStorage.todos))
} else {
localStorage.todos = []
}
// localStorage.todos && console.log('a1:',JSON.parse(localStorage.todos))
}, [])
useEffect(() => {
if (todos && todos.length > 0) {
localStorage.todos && console.log('b2:',JSON.parse(localStorage.todos))
localStorage.setItem("todos", JSON.stringify(todos))
localStorage.todos && console.log('a2:',JSON.parse(localStorage.todos))
}
}, [todos])
function handleSubmit(e) {
e.preventDefault()
const id = uid()
setTodos([{text, done:false, id:id}, ...todos])
setText("")
}
function markDone(e) {
setTodos(todos.map(todo => {
if (e.target.innerText === todo.text) {
return {...todo, done:!todo.done}
} else {
return todo
}
}))
}
function deleteTodo(id) {
setTodos(todos.filter((todo,i,arr) => {
console.log("id:", id)
console.log("todo.id:",todo.id)
console.log("are equal:", todo.id === id)
console.log(i, arr)
return (todo.id !== id)
}))
}
function deleteAll() {
setTodos([])
setShowClearTodosModal(false)
localStorage.removeItem("todos")
}
return (
<Box>
<form
onSubmit={handleSubmit}
>
<TextField
className={styles.entryfield}
label="Add a todo"
autoFocus
value={text}
onChange={(e) => setText(e.target.value)}
ref={inputRef}
/>
</form>
<Button
color="primary"
aria-label="upload picture"
component="span"
className={styles.clearall}
onClick={() => setShowClearTodosModal(true)}
startIcon={<ReportProblemIcon />}
>
Delete All Todos
</Button>
<Modal
open={showClearTodosModal}
onClose={() => setShowClearTodosModal(false)}
aria-labelledby="delete all todos"
aria-describedby="delete all todos"
className={styles.deleteallmodal}
>
<Box className={styles.modalbox}>
<Typography id="modal-modal-title" variant="h6" component="h2">
Delete all todos?
</Typography>
<Typography id="modal-modal-description" sx={{ mt: 2 }}>
Are you sure you want to delete all todos? This action is irreversable and you will lose all of your todos.
</Typography>
<Button
onClick={() => setShowClearTodosModal(false)}
variant="contained"
sx={{m:1}}
>
Nah, don't delete my todos
</Button>
<Button
onClick={deleteAll}
variant="contained"
startIcon={<ReportProblemIcon />}
sx={{
m:1,
color: "maroon",
}}
>
Yes I'm sure delete all of them
</Button>
</Box>
</Modal>
<List>
{todos && todos.map(todo =>(
<ListItem
key={todo.id}
onClick={markDone}
>
<Card className={styles.card}
>
<IconButton
className={styles.icons}
onClick={() => {deleteTodo(todo.id)}}
aria-label="delete"
>
<DeleteIcon fontSize="small"/>
</IconButton>
<IconButton
className={styles.icons}
aria-label="edit"
>
<EditIcon fontSize="small"/>
</IconButton>
<Typography
variant="body1"
style= {{
color: todo.done ? "#555" : "",
margin: 10,
}}
>
{todo.text}
</Typography>
</Card>
</ListItem>
)
)}
</List>
</Box>
)
}
Here is the working codesandbox of your component. Please align your code with my example and be sure that you do not have any other wrong implementations in your code
https://codesandbox.io/s/heuristic-shadow-7lz1o0?file=/src/App.js
import { useState } from "react";
import "./styles.css";
export default function App() {
const [todos, setTodos] = useState([
{ text: "two", done: false, id: "7dbcd88d5a3" },
{ text: "one", done: false, id: "37dbcd88d5a" },
{ text: "one", done: false, id: "643dbcd88d5a" }
]);
function deleteTodo(id) {
setTodos(
todos.filter((todo, i, arr) => {
console.log("id:", id);
console.log("todo.id:", todo.id);
console.log("are equal:", todo.id === id);
console.log(i, arr);
return todo.id !== id;
})
);
}
return (
<div className="App">
{todos &&
todos.map((todo) => (
<div key={todo.id}>
<div>
<button
onClick={() => {
deleteTodo(todo.id);
}}
aria-label="delete"
>
{todo.id}
</button>
</div>
</div>
))}
</div>
);
}
let testArray = [{text: "two", done: false, id: "7dbcd88d5a3"}, {text: "one", done: false, id: "37dbcd88d5a"},
{text: "one", done: false, id: "643dbcd88d5a"}]
function deleteTodo(id) {
console.log(testArray.filter((todo,i,arr) => {
return (todo.id !== id)}))
}
deleteTodo('7dbcd88d5a3');
Thanks for sharing your full code! It was helpful to understand what was happening.
The culprit line that's causing you the headache is here:
<ListItem
key={todo.id}
onClick={markDone}
>
<Card className={styles.card}
>
<IconButton
className={styles.icons}
onClick={() => {deleteTodo(todo.id)}}
aria-label="delete"
>
Specifically, onClick={markDone} on ListItem. You put the markDone function on the whole list item. So clicking on the IconButton causes the ListItem event to be triggered too.
I know you have the e.preventDefault() but unfortunately the order is reversed here with event bubbling (starts inside out and you need stopPropagation instead of preventDefault): I put some logs in deleteTodo and markDone to indicate the start and end of each function and this is the order. This was the output:
Todos.tsx:82 deleteTodo start
Todos.tsx:86 deleteTodo end
Todos.tsx:66 markdone start
Todos.tsx:76 markdone end
So what's likely happening is react is either doing the setTodos sequentially or batching them, but either way it looks like the setTodos from markDone is taking precedence, and the setTodo there is just a map of the same values again. Hence why clicking the delete button leads to no changes and the same todo items remaining.
Possible solutions could include
a) Add the event as a parameter to deleteTodo and run e.stopPropagation() at the top of the function to prevent event bubbling further.
function deleteTodo(e, id) {
e.stopPropagation();
console.log('deleteTodo start');
setTodos(
todos.filter((todo, i, arr) => {
return todo.id !== id;
})
);
console.log('deleteTodo end');
}
and setup your IconButton:
<IconButton
onClick={(e) => {
deleteTodo(e, todo.id);
}}
This should help stop the event from bubbling further up (I confirmed this to be working locally).
b) You could remove the onClick={markDone} and add a separate button to do the mark done action (removing the onClick is confirmed to make your delete method work)
c) You keep the markDone but do some additional conditional checking on the e event target to see if the delete button was hit - if it was, return early in the function.
export default function SearchPage() {
const [searchString, setSearchString] = React.useState("");
const [apiCall, setApiCall] = React.useState<() => Promise<Collection>>();
const {isIdle, isLoading, isError, error, data} = useApi(apiCall);
const api = useContext(ApiContext);
useEffect(()=>console.log("APICall changed to", apiCall), [apiCall]);
const doSearch = (event: React.FormEvent) => {
event.preventDefault();
setApiCall(() => () => api.search(searchString));
};
const doNext = () => {
var next = api.next;
if (next) {
setApiCall(()=>(() => next)());
}
window.scrollTo(0, 0);
}
const doPrev = () => {
if (api.prev) {
setApiCall(() => api.prev);
}
window.scrollTo(0, 0);
}
return (
<>
<form className={"searchBoxContainer"} onSubmit={doSearch}>
<TextField
label={"Search"}
variant={"filled"}
value={searchString}
onChange={handleChange}
className={"searchBox"}
InputProps={{
endAdornment: (
<IconButton onClick={() => setSearchString("")}>
<ClearIcon/>
</IconButton>
)
}}
/>
<Button type={"submit"} variant={"contained"} className={"searchButton"}>Go</Button>
</form>
{
(isIdle) ? (
<span/>
) : isLoading ? (
<span>Loading...</span>
) : isError ? (
<span>Error: {error}</span>
) : (
<Paper className={"searchResultsContainer"}>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
<Box className={"searchResults"}>
{
data && data.items().all().map(item => (
<span className={"thumbnailWrapper"}>
<img className={"thumbnail"}
src={item.link("preview")?.href}
alt={(Array.from(item.allData())[0].object as SearchResponseDataModel).title}/>
</span>
))
}
</Box>
<Box className={"navButtonContainer"}>
<Button variant={"contained"}
disabled={!api.prev}
onClick={doPrev}
className={"navButton"}>
{"< Prev"}
</Button>
<Button variant={"contained"}
disabled={!api.next}
onClick={doNext}
className={"navButton"}>
{"Next >"}
</Button>
</Box>
</Paper>
)
}
</>
)
}
For various reasons, I've got a function stored in my state (it's for use with the react-query library). I'm seeing very odd behaviour when I try and update it, though. When any of doSearch, doNext, or doPrev are called, it successfully updates the state - the useEffect hook is firing properly and I can see the message in console - but it's not triggering a re-render until the window loses and regains focus.
Most of the other people I've seen with this problem have been storing an array in their state, and updating the array rather than creating a new one - so the hooks don't treat it as a new object, and the re-render doesn't happen. I'm not using an array, though, I'm using a function, and passing it different function objects. I'm absolutely stumped and have no idea what's going on.
EDIT: It seems it might not be the rendering failing to fire, but the query hook not noticing that its input has changed? I've edited the code above to show the whole function, and my custom hook is below.
function useApi(func?: () => Promise<Collection>) {
return useQuery(
["doApiCall", func],
func || (async () => await undefined),
{
enabled: !!func,
keepPreviousData: true
}
)
}
You can’t put a function into the queryKey. Keys need to be serializable. See: https://react-query.tanstack.com/guides/query-keys#array-keys
I want to click X Button in Card extra to visible "Confirm Remove Todo modal".
UI:
But...
the reality when I click X Button then it visible "Edit Todo modal" from Card event instead.
how can I fix it?
Code:
{todos.map(todo => (
<Card
className={styles.CardTodo}
headStyle={{ textAlign: 'left' }}
bodyStyle={{ textAlign: 'left' }}
key={todo._id}
title={todo.title}
onClick={() => handleSelectTodo(todo._id)}
extra={
<Button
type="danger"
shape="circle"
style={{ color: 'white', zIndex: 10 }}
onClick={() => handleRemoveTodo(todo._id)}
>
X
</Button>
}
>
{todo.description}
</Card>
))}
.
.
Thanks very much, guys
e.stopPropagation() is useful for me.
And then I found another problem.
It is handleRemoveTodo() is the function that opens another modal.
But that modal didn't get "Todo object"
when I remove e.stopPropagation(), the modal will get Todo Object again
Code:
Todo component
const handleRemoveTodo = () => {
setModalConfirmRemoveVisible(true)
}
const handleConfirmRemove = async todoId => {
console.log('Hello', todoId)
setIsRemoveLoading(true)
try {
await axios.delete(`/todos/${todoId}`, apiConfig)
} catch (err) {
console.error(err)
console.error(err.response.data)
}
await fetchTodos()
setModalConfirmRemoveVisible(false)
setIsRemoveLoading(false)
}
return (
{modalConfirmRemoveVisible && (
<ModalConfirmRemoveTodo
visible={modalConfirmRemoveVisible}
todo={todo}
isRemoveLoading={isRemoveLoading}
onConfirmRemoveTodo={handleConfirmRemove}
onCancel={() => setModalConfirmRemoveVisible(false)}
onConfirmRemove={handleConfirmRemove}
/>
)}
)
Modal component
const ModalConfirmRemoveTodo = props => {
const { visible, isRemoveLoading, onCancel, todo, onConfirmRemove } = props
console.log('ModalConfirmRemoveTodo', todo)
return (
<>
<Modal
visible={visible}
title={<Title level={3}>Remove Todo</Title>}
okButtonProps={{ loading: isRemoveLoading, disabled: isRemoveLoading }}
okText="Remove"
okType="danger"
onOk={() => onConfirmRemove(todo._id)}
onCancel={onCancel}
>
Want delete {todo.title} ?
</Modal>
</>
)
}
This is called Event Bubbling. When an event happens on an element, it first runs the handlers on it, then on its parent, then all the way up on other ancestors.
Please refer to this article for details: https://javascript.info/bubbling-and-capturing#bubbling
Below is my solution to your problem. Instead of opening a modal, I just use a simple alert to simulate it.
Your current problem: https://codesandbox.io/s/event-bubbling-bojvq
You will see that the Chrome alert will pop up twice. The former is from the onClick of extra, the latter is from onClick of Card.
Solution: https://codesandbox.io/s/prevent-bubbling-zkxk6
Just add a simple e.stopPropagation() to prevent the bubbling inside extra Button onClick. Please refer to this: https://javascript.info/bubbling-and-capturing#stopping-bubbling for more information.
Back to your code, just simply update your Button's onClick like this:
onClick={e => { e.stopPropagation(); handleRemoveTodo(todo._id)}}
Use stopPropagation() method on your event:
<Button
type="danger"
shape="circle"
style={{ color: 'white', zIndex: 10 }}
onClick={e => { e.stopPropagation(); handleRemoveTodo(todo._id)}}
>
X
</Button>
I have a problem with react-bootstrap-sweetalert library in react. Actually it works fine, untill slow internet connection. When someone tries to click submit button, because of the slow internet (I'm simulating it through "Network section [Slow 3G]") alert is not closing exactly at time after clicking a button, but after several seconds. So, there is probability that someone can click several times submit button. It is a problem, because several same requests can flow to backend and database. In other sections without using a library I can just "disable" react states after handling onClick.
So question is - to disable button in react-bootstrap-sweetalert library after handling onConfirm function.
Code:
handleSubmitInvoice = () => {
this.setState({
sweetalert: (
<SweetAlert
warning
showCancel
confirmBtnText={this.state.alert.label.sure}
cancelBtnText={this.state.alert.label.cancel}
confirmBtnBsStyle="success"
cancelBtnBsStyle="default"
disabled={disableButton}
title={this.state.alert.label.areyousure}
onConfirm={() => this.submit()}
onCancel={() => this.hideAlert()}
>
{this.state.alert.confirmSubmit}
</SweetAlert>
)
});
};
in render():
<button
className="btn btn-success btn-sm"
onClick={this.handleSubmitInvoice}
>
submit
</button>
submit function:
submit = () => {
const req = { invoice: this.state.invoiceNumber };
Axios.post("/api", req)
.then(() => {
this.props.history.push({
pathname: "/mypathname",
state: {
fromSubmitInvoice: true
}
});
})
.catch(err => {
Alert.error(
err.response.data.code === "internal_error"
? this.state.alert.raiseError
: err.response.data.text,
{
position: "top-right",
effect: "bouncyflip",
timeout: 2000
}
);
this.hideAlert();
});
};
Codesandbox: https://codesandbox.io/s/sweet-alert-problem-ktzcb
Thanks in advance.
Problem solved try this out
import React, { Component } from "react";
import SweetAlert from "react-bootstrap-sweetalert";
import ReactDOM from "react-dom";
const SweetAlertFunction = ({ show, disableButton, submit, hideAlert }) => {
return (
<SweetAlert
warning
show={show}
showCancel
confirmBtnText="confirmBtnText"
cancelBtnText="cancelBtnText"
confirmBtnBsStyle="success"
cancelBtnBsStyle="default"
disabled={disableButton}
title="title"
onConfirm={submit}
onCancel={hideAlert}
>
submit
</SweetAlert>
);
};
export default class HelloWorld extends Component {
constructor(props) {
super(props);
this.state = {
disableButton: false,
show: false
};
}
hideAlert() {
this.setState({
show: false
});
}
submit() {
this.setState({ disableButton: true });
console.log("submit");
setTimeout(() => {
this.setState({ disableButton: false });
}, 3000);
}
render() {
const { show, disableButton } = this.state;
console.log("disableButton", disableButton);
return (
<div style={{ padding: "20px" }}>
<SweetAlertFunction
show={show}
disableButton={disableButton}
submit={() => this.submit()}
hideAlert={() => this.hideAlert()}
/>
<button
className="btn btn-success btn-sm"
onClick={() => this.setState({ show: true })}
>
Click
</button>
</div>
);
}
}
ReactDOM.render(<HelloWorld />, document.getElementById("app"));
In your case, since you are assigning the Sweetalert component to the sweetalert state, you need to have a local state that controls the disabled state, but to make it simple, you can make sweetalert state control the visibility/presence of the Sweetalert component, like below:
handleSubmitInvoice() {
// just set sweetalert to true to show the Sweetalert component
this.setState({ sweetalert: true });
}
render() {
const { sweetalert, disableButton } = this.state;
return (
<div style={{ padding: "20px" }}>
// this makes disableButton reactive and pass it automatically to Sweetalert component
{sweetalert && (
<SweetAlert
warning
showCancel
confirmBtnText="confirmBtnText"
cancelBtnText="cancelBtnText"
confirmBtnBsStyle="success"
cancelBtnBsStyle="default"
disabled={disableButton}
title="title"
onConfirm={() => this.submit()}
onCancel={() => this.hideAlert()}
>
submit
</SweetAlert>
)}
<button
className="btn btn-success btn-sm"
onClick={this.handleSubmitInvoice}
>
Click
</button>
</div>
);
}
You can see it in this sandbox https://codesandbox.io/s/sweet-alert-problem-lv0l5
P.S. I added setTimeout in submit to make disabling of button noticeable.
Action overriding feature allows me to override button but it overrides all the action buttons. For example, if I have two action buttons for edit and delete and I use the Action overriding, both of my buttons get overridden with that same custom code.
How can I specify different codes for different buttons?
My final goal is to conditionally disable edit and delete buttons based on the rowData. I have tried with isEditable feature as shown in the code below but it doesn't work either.
...
....
const components = {
Action: props => (
<IconButton aria-label="delete" size="small"
disabled={props.data.email === 'admin#pipilika.com'}
onClick={(event) => props.action.onClick(event, props.data)}
>
<Icon>delete</Icon>
</IconButton>
)
};
const actions = [
{
icon: 'edit',
tooltip: 'Edit Index',
onClick: (event, rowData) => {
this.onEditClick(null, rowData._id);
}
},
{
icon: 'delete',
tooltip: 'Delete Index',
onClick: (event, rowData) => {
this.onDeleteClick(null, rowData._id);
}
},
];
const options= {
showTitle: false,
actionsColumnIndex: -1,
searchFieldStyle: {
color: "#fff"
}
};
const editable= {
isEditable: rowData => rowData.dataType === "html",
isDeletable: rowData => rowData.dataType === "html",
};
return(
<MaterialTable
editable={editable}
title="Created Index List"
columns={columns}
data={dataTypes}
actions={actions}
options={options}
components={components}
style={{overflow: 'hidden'}}
/>
);
for this specific use case, you can choose which Button to render based on checking the right icon name like this.
components={{
Action:
props => {
if(props.action.icon === 'edit'){
return(
<Button
onClick={(event) => props.action.onClick(event, props.data)}
color="primary"
variant="contained"
style={{textTransform: 'none'}}
size="small"
disabled
>
My Button
</Button>
)
}
if(props.action.icon === 'save'){
return(
<Button
onClick={(event) => props.action.onClick(event, props.data)}
color="primary"
variant="contained"
style={{textTransform: 'none'}}
size="small"
>
My Button
</Button>
)
}
}
}}
This solution worded form me:
import React, { Component } from 'react';
import {Button} from '#material-ui/core/';
import Icon from '#material-ui/core/Icon';
class ButtonBack extends Component {
constructor(props) {
super(props);
this.state={
onClick: props.onClick,
icon: props.icon
}
};
render() {
return (
<Button label="Regresar" onClick={this.state.onClick}>
<Icon>{this.state.icon}</Icon>
Regresar
</Button>
);
}
}
export default ButtonBack;
components={{
Action: props => {
if (typeof props.action === "function"){
var element= props.action(props.data);
return (
<IconButton aria-label={element.icon} size="small"
onClick={element.onClick}
>
<Icon>{element.icon}</Icon>
</IconButton>
)
}else{
if (props.action.icon==="keyboard_backspace"){
return (
<ButtonBack icon={props.action.icon}
onClick={props.action.onClick}
>
</ButtonBack>
)
}else{
return (
<IconButton aria-label={props.action.icon} size="small"
onClick={props.action.onClick}
>
<Icon>{props.action.icon}</Icon>
</IconButton>
)
}
}
}
}}