-
-
Save Rahul-RB/273dbb24faf411fa6cc37488e1af2415 to your computer and use it in GitHub Desktop.
/* No licenses, use as pleased. | |
* The code here uses React Class components (ES6 classes). | |
* Ken Nguyen has made a hooks version of this! Please find that here: https://codesandbox.io/s/addanddelete-tabs-mui-bo7tw | |
* Cheers! | |
*/ | |
import React, { Component } from "react"; | |
import { | |
withStyles, | |
AppBar, | |
Tabs, | |
Tab, | |
Grid, | |
Button | |
} from "@material-ui/core"; | |
import Add from "@material-ui/icons/Add"; | |
import Close from "@material-ui/icons/Close"; | |
import cloneDeep from "lodash/cloneDeep"; | |
const styles = theme => ({ | |
root: { | |
flexGrow: 1, | |
marginTop:"60px", | |
width: "100%", | |
backgroundColor: theme.palette.background.paper | |
}, | |
appBar:{ | |
color:"inherit", | |
backgroundColor: theme.palette.background.paper | |
} | |
}); | |
class CustomTabs extends Component { | |
constructor(...args){ | |
super(...args); | |
this.state = { | |
value: 0, | |
tabList : [{ | |
key:0, | |
id:0, | |
}] | |
}; | |
} | |
addTab = () => { | |
this.setState((state,props)=>{ | |
let tabList = cloneDeep(state.tabList); | |
let id = tabList[tabList.length-1].id+1; | |
tabList.push({ | |
key:id, | |
id:id, | |
}); | |
return { | |
tabList, | |
} | |
}) | |
} | |
deleteTab = (e) => { | |
// prevent MaterialUI from switching tabs | |
e.stopPropagation(); | |
// Cases: | |
// Case 1: Single tab. | |
// Case 2: Tab on which it's pressed to delete. | |
// Case 3: Tab on which it's pressed but it's the first tab | |
// Case 4: Rest all cases. | |
// Also cleanup data pertaining to tab. | |
// Case 1: | |
if(this.state.tabList.length === 1){ | |
return; // If you want all tabs to be deleted, then don't check for this case. | |
} | |
// Case 2,3,4: | |
let tabID = parseInt(e.target.id); | |
let tabIDIndex = 0; | |
let tabList = this.state.tabList.filter((value,index)=>{ | |
if(value.id === tabID){ | |
tabIDIndex = index; | |
} | |
return value.id !== tabID; | |
}); | |
this.setState((state,props)=>{ | |
let curValue = parseInt(state.value); | |
if(curValue === tabID){ | |
// Case 3: | |
if(tabIDIndex === 0){ | |
curValue = state.tabList[tabIDIndex+1].id | |
} | |
// Case 2: | |
else{ | |
curValue = state.tabList[tabIDIndex-1].id | |
} | |
} | |
return { | |
value:curValue | |
} | |
},()=>{ | |
this.setState({ | |
tabList:tabList | |
}) | |
}); | |
} | |
handleTabChange = (event, value) => { | |
this.setState({ value }); | |
} | |
render() { | |
const { classes } = this.props; | |
const { value } = this.state; | |
// console.log(this.state); | |
return ( | |
<AppBar position="static" className={classes.appBar}> | |
<Grid | |
container | |
alignItems="center" | |
justify="center" | |
> | |
<Grid | |
item | |
xl={11} | |
lg={11} | |
md={11} | |
sm={11} | |
xs={11} | |
> | |
<Tabs | |
value={value} | |
onChange={this.handleTabChange} | |
indicatorColor="primary" | |
textColor="primary" | |
variant="scrollable" | |
scrollButtons="auto" | |
> | |
{ | |
this.state.tabList.map((tab)=>( | |
<Tab | |
key={tab.key.toString()} | |
value={tab.id} | |
label={"Node "+tab.id} | |
icon={ | |
<Close | |
id={tab.id} | |
onClick={ | |
this.deleteTab | |
} | |
/> | |
} | |
className="mytab" | |
/> | |
)) | |
} | |
</Tabs> | |
</Grid> | |
<Grid | |
item | |
xl={1} | |
lg={1} | |
md={1} | |
sm={1} | |
xs={1} | |
> | |
<Button | |
variant="outlined" | |
onClick={this.addTab} | |
> | |
<Add/> | |
</Button> | |
</Grid> | |
</Grid> | |
</AppBar> | |
); | |
} | |
} | |
export default withStyles(styles)(CustomTabs); |
Hi,
So as I mention in the comments, there's 4 cases I had to handle:
- Single tab in the entire app bar : for this its your call, I chose not to delete the only tab.
- The delete button of the currently focused tab is pressed: In this case you need to delete the tab and move one tab back (thus
tabIDIndex -1
). If this was the only tab then it'd be caught in Case 1 itself. - The delete button of the currently focused tab is pressed BUT this is the first tab. I cannot move "back" one tab (i.e.
tabIDIndex - 1
would be wrong) so I just move ahead one tab. - All other cases.
So tabIDIndex helps me identify Case 2 and 3 and choose the next tab to focus accordingly.
Now, coming to the local variable tabList
, it's been defined at line 75 and used at line 98. The local variable tabList
holds all the "tabs" except for the one who's delete was clicked. Thus I just reassign the tabList this.state
in line 98 to the new tabList.
Hope this clears out some stuff.
Also, let me know if you are interested in making the 'x' icon (the delete icon) to appear on the right of the tab instead of top. It requires a small modification to the material-ui source code itself.
You can watch a video of the exact code as above working here: https://www.loom.com/share/9acfa8fa53fc45eba8ba3ec1c1a7eb79
Hi @bravemaster619 , no issues, glad to help! :)
Ok, so I would do this hack in node_modules/@material-ui/core/Tab/Tab.js
at the return
of function styles
, by replacing the flexDirection:column
with flexDirection: row-reverse
. It works in Material UI 3.9.3, but in the latest one I checked, this hack no longer works.
Of course changing node_modules is not the solution, so I guess this needs to be a feature request. Tab
component can take a property called "iconAlignment" where we can specify the flex direction.
Or, you can just extend
Tab
class and use it as your custom tab.
And also, the following css trick would simply do the work:.MuiTab-wrapper { flex-direction: row-reverse; }
Hope
@material-ui
may include icon position feature in a future release.
Ohh, this is a better idea. Will try it out. Adding CSS directly may not work in production, might need to check that out.
And by the way, what about using
redux
?
Actually, I needed dynamic tabs in my application so I googled and found your work.
At first, your code was not working because of some minor mistakes. After you fixed your code, it worked but it didn't matter to me. 'Cause I usedredux
in my code.
Adding/removing empty tabs is meaningless. Tabs must be linked to some contents.
That's why you should manage tabs and related contents simultaneously. But it's somewhat tedious.
Usingredux
greatly simplified my work.
I think if you useredux
, you won't need to handle four cases yourself. Just change your state andredux
will arrange everything.
What do you think?
Yes, redux will make state management much simpler. In my solution, as you mention, maintaining tab and its contents are very tedious if the tabs gets even deep (say more dynamic lists within tabs which is totally possible). But every project I've done using react was small and really didn't require the powers and simplicity of redux.
Could you post a gist which does dynamic tabs with redux?
Project looks and sounds very interesting!
Ok, so I saw tried this stackoverflow and it works as expected. Also, I tried the CSS idea for making the icon appearing on right, i.e.:
.MuiTab-wrapper {
flex-direction: row-reverse!important;
}
This works too, adding a bit of padding and margin aligns it in between:
.MuiSvgIcon-root {
padding-left: 10px!important;
margin-top: 5px;
font-size: 20px!important;
}
Hi @Rahul-RB, thank you for the work. I was struggling finding a way to delete tabs with MUI and I found yours. It worked perfectly. Since your code is working with React Class Component, I have re-written it with React Functional Component and Hooks.
Here is a reproduction. I also mentioned your work there :D
Hi @Rahul-RB, thank you for the work. I was struggling finding a way to delete tabs with MUI and I found yours. It worked perfectly. Since your code is working with React Class Component, I have re-written it with React Functional Component and Hooks.
Here is a reproduction. I also mentioned your work there :D
Hi,
No issues and thanks for the mention!
I think the Hooks version would server better for future proofing (given its usefulness in unit testing and ease of reading).
I'll link your codesandbox.io link in my gist.
Hi! Your code helped me out for real! Thanks a lot for sharing.
I have a suggestion. When you click on an 'x' button, sometimes it doesn't work because of the svg children <path/>
tag. So I added this in the styles so that the onClick event doesn't trigger it.
path{
pointer-events: none;
}
Took the hint from this article.
@atomash13
Ah yes, I did observe that the X button didn't work exactly, thank you for pointing out a fix!
Happy to help out! :)
Hey @Rahul-RB ,
First of all thanks you for posting such amazing code.
In the lines 87 - 107, you've used a second callback..
but in my code I'm not using class component.
So, I'm getting error "Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect()."
What will be the possible way to do so without callback. Can you please help me out of this.
Thanks in advance.
@yadavendra15 thank you, could you please "star" this gist.
I remember @KenNguyen-0107 had created a hooks version of this here: https://codesandbox.io/s/addanddelete-tabs-mui-bo7tw
Could you try that and let me know if that works?
Sure Rahul,
Thanks for it
Hey @Rahul-RB ,
Can you Please help me in https://codesandbox.io/s/thirsty-joliot-pjhkd?file=/src/AddAndDeleteTab.js
actually, I want to add a Whiteboard in those tabs..
Now, I'm stuck at one point..
When I draw something in the whiteboard and switch the tabs, then my tabpanel re-renders, but I don't want to do so. I want to avoid the re-render of tabpanel.
And my second issue is, Suppose I draw something on WhiteBoard1 and I close that tab 1.. then, that content will be shifted to the next tab, and shows as the content of tab 2..
Can the tabs be renamed?
hey @Rahul-RB do we have an example where there are contents to these tabs?
Hi @bravemaster19 , thanks for pointing it out, fixed it just now.