Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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);
@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 4, 2019

Hi. Check out code snippet from line 74 to 81. You used undefined variables, such as tabID and tabIDIndex.
And your code is not working because of this (when you're trying to remove a tab). I think it needs fixing.

@Rahul-RB

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB Rahul-RB commented Nov 4, 2019

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

@Rahul-RB

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB 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

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB 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

@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 6, 2019

Hi, @Rahul-RB, Thank you for your nice reply. I just made a mistake and overlooked some of your code. I've already removed wrong comments of mine. Good job! Your code works perfectly.
P.S. I'm really interested in your suggestion:

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

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB 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.

@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 6, 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.

@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 6, 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?

@Rahul-RB

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB 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

This comment has been minimized.

Copy link
Owner Author

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

@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 8, 2019

Hi, @Rahul-RB. Thanks for your reply. I think your opinion is right.
As a matter of fact, I'm currently working on a project that combines Google Map and Voxeet (live video/audio and cobrowsing service).

image

It would be really tedious if I hadn't used redux in my app.
But in simple apps, just setState would do the work.
And as a second thought, I realized I had to deal with four cases even though I used redux.

Could you post a gist which does dynamic tabs with redux?

It's an ongoing project and I'm still working on it. I'll git my project once it's done.

P.S. How do you adjust the width of tabs? I think they're a little bit wide. I'd really appreciate if you advise me how to. Thanks in advance.

@Rahul-RB

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB 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;
}
@bravemaster619

This comment has been minimized.

Copy link

@bravemaster619 bravemaster619 commented Nov 13, 2019

Hi, @Rahul-RB, I've found a better way to customize material-ui tabs.
It's using withStyles. Follow this link: How to extends material-ui component
I think it's the only correct and effective way.

@KenNguyen-0107

This comment has been minimized.

Copy link

@KenNguyen-0107 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

This comment has been minimized.

Copy link
Owner Author

@Rahul-RB Rahul-RB 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

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.

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