Skip to content

Instantly share code, notes, and snippets.

@Rahul-RB
Last active March 26, 2024 15:03
Show Gist options
  • Save Rahul-RB/273dbb24faf411fa6cc37488e1af2415 to your computer and use it in GitHub Desktop.
Save Rahul-RB/273dbb24faf411fa6cc37488e1af2415 to your computer and use it in GitHub Desktop.
Dynamically add and remove tabs in Material UI (Browser tabs feature)
/* 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);
@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 4, 2019

Hi @bravemaster19 , thanks for pointing it out, fixed it just now.

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 4, 2019

Hi,

So as I mention in the comments, there's 4 cases I had to handle:

  1. Single tab in the entire app bar : for this its your call, I chose not to delete the only tab.
  2. 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.
  3. 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.
  4. 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.

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 4, 2019

You can watch a video of the exact code as above working here: https://www.loom.com/share/9acfa8fa53fc45eba8ba3ec1c1a7eb79

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 6, 2019

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.

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 7, 2019

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.

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 7, 2019

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 used redux 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.
Using redux greatly simplified my work.
I think if you use redux, you won't need to handle four cases yourself. Just change your state and redux 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?

@Rahul-RB
Copy link
Author

Rahul-RB commented Nov 9, 2019

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;
}

@KenNguyen-0107
Copy link

KenNguyen-0107 commented Mar 27, 2020

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

https://codesandbox.io/s/addanddelete-tabs-mui-bo7tw

@Rahul-RB
Copy link
Author

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

https://codesandbox.io/s/addanddelete-tabs-mui-bo7tw

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.

@atomash13
Copy link

atomash13 commented Aug 3, 2021

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.

@Rahul-RB
Copy link
Author

Rahul-RB commented Aug 3, 2021

@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! :)

@yadavendra15
Copy link

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.

@Rahul-RB
Copy link
Author

@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?

@yadavendra15
Copy link

Sure Rahul,
Thanks for it

@yadavendra15
Copy link

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..

Please help me in this..

@smarajitWOW
Copy link

Can the tabs be renamed?

@pereiraryan
Copy link

hey @Rahul-RB do we have an example where there are contents to these tabs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment