Add dynamic value beside label in Material UI React Tabs Component - reactjs

I am creating tabs using material ui and i want to add value beside label (ex: 05 written beside Recommendation label) which will be dynamic in the tabs component just like shown in below image:
Here is my code snippet of Tabs component:
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
<Tab label="Recommendation" />
<Tab label="Ongoing" />
<Tab label="Completed" />
</Tabs>
</Box>

One option is to create your own component, pass that in as a the Tab label, and then style it as needed. For example:
const TabWithCount = ({ children, count }) => {
return (
<Box sx={{ display: "inline-flex", alignItems: "center" }}>
<Typography component="div">{children}</Typography>
{count ? (
<Typography
component="div"
variant="body2"
sx={{ marginLeft: "0.5rem" }}
>
{count}
</Typography>
) : null}
</Box>
);
};
...
<Tab
label={<TabWithCount count="05">Recommendation</TabWithCount>}
/>
I've created a quick (unstyled) example to illustrate the solution: https://codesandbox.io/s/basictabs-material-demo-forked-i9543?file=/demo.js

Related

How to make MUI Tab to have multiple routes

I'm trying to make a simple navigation component with Material UI, React Router v6 and TypeScript. I have AppBar component with Tabs inside, it looks like this:
NavBar screenshot
The layout component renders the <Navigation /> component that contains what I mentioned above and the <Outlet /> to render the component according to the current path.
The problem is that I have two components for user: one to view users (its like a table that lists the users) and the second is a screen to create users (routes are "/secure/users" and the other is "/secure/userCreate"). When I click on the "Users" tab, the userView component gets rendered, because as u can see in the code below, I have the tab
<Tab label="Users" value={routes[6]} to="/secure/users" component={Link} />
to match the user view route ("/secure/users"), but that component has a button to create a new user, it redirects you to "/secure/userCreate" , but that way MUI tab gives me an error because there is no Tab that matches that route.
So my question is, how can I make the Users tab stay selected when I'm in the userView component and then also be selected on user creation? Because I think that would need the tab to match both routes at the same time, "/secure/users" and "/secure/userCreate", and I dont know how to do that or if its possible
Here's my code so far:
navigation.component.tsx
const routes = ["/secure/books", "/secure/offers", "/secure/products", "/secure/vacation", "/secure/others", "/secure/reports", "/secure/users"];
function useRouteMatch(patterns: readonly string[]) {
const { pathname } = useLocation();
for (let i = 0; i < patterns.length; i += 1) {
const pattern = patterns[i];
const possibleMatch = matchPath(pattern, pathname);
if (possibleMatch !== null) {
return possibleMatch;
}
}
return null;
}
export const Navigation = (): ReactElement => {
const permission = getPermission();
const routeMatch = useRouteMatch(routes)
const currentTab = routeMatch?.pattern?.path;
return (
<Container style={{ marginTop: 34}}>
<AppBar
position="static"
style= {{ background: '#FFFFFF'}}
elevation={0}
>
<Container maxWidth="xl">
<Tabs
value={currentTab}
aria-label="nav tabs example"
variant="scrollable"
scrollButtons="auto"
sx={{
'& .MuiTab-root': {
fontSize: 18,
fontWeight: 600,
lineHeight: '175%',
letterSpacing: '0.15px',
padding: '5px 0px 5px 0px',
color: 'rgba(0, 54, 101, 0.6)',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
marginRight: 6.25,
minWidth: 0,
},
'& .MuiTabs-indicator': {
backgroundColor: "#01A3FE",
},
'& .Mui-selected': {
color: "#01A3FE !important",
}
}}
>
<Tab label="Books" value={routes[0]} to="/secure/books" component={Link} />
<Tab label="Offers" value={routes[1]} to="/secure/offers" component={Link} />
<Tab label="Products" value={routes[2]} to="/secure/products" component={Link} />
<Tab label="Vacation" value={routes[3]} to="/secure/vacation" component={Link} />
<Tab label="Others" value={routes[4]} to="/secure/others" component={Link} />
<Tab label="Reports" value={routes[5]} to="/secure/reports" component={Link} />
{permission.isAdmin &&
<Tab label="Users" value={routes[6]} to="/secure/users" component={Link} />}
<span className="MuiTabs-indicator css-1aquho2-MuiTabs-indicator" style={{ left: 0, backgroundColor: '#EEEEEE'}}></span>
</Tabs>
</Container>
</AppBar>
</Container>
);
}
layout.component.tsx
export const Layout = (): ReactElement => {
return (
<div>
<Navigation />
<Outlet />
</div>
);
}
Edit: Maybe to clarify a bit, I need the Users Tab to stay selected when I'm in both routes ("/secure/users" and "/secure/userCreate").
You could simply check if the current pathname value is the "/secure/userCreate" path and set the currentTab value to equal that of the "Users" tab's URL path.
Example:
const Navigation = (): ReactElement => {
const { pathname } = useLocation();
const permission = getPermission();
let currentTab = pathname;
if (pathname === "/secure/userCreate") {
currentTab = "/secure/users";
}
return (
<Container style={{ marginTop: 34 }}>
<AppBar position="static" style={{ background: "#FFFFFF" }} elevation={0}>
<Container maxWidth="xl">
<Tabs
value={currentTab}
aria-label="nav tabs example"
variant="scrollable"
scrollButtons="auto"
sx={{ .... }}
>
<Tab
label="Books"
value={routes[0]}
to="/secure/books"
component={Link}
/>
<Tab
label="Offers"
value={routes[1]}
to="/secure/offers"
component={Link}
/>
<Tab
label="Products"
value={routes[2]}
to="/secure/products"
component={Link}
/>
<Tab
label="Vacation"
value={routes[3]}
to="/secure/vacation"
component={Link}
/>
<Tab
label="Others"
value={routes[4]}
to="/secure/others"
component={Link}
/>
<Tab
label="Reports"
value={routes[5]}
to="/secure/reports"
component={Link}
/>
{permission.isAdmin && (
<Tab
label="Users"
value={routes[6]}
to="/secure/users"
component={Link}
/>
)}
<span
className="MuiTabs-indicator css-1aquho2-MuiTabs-indicator"
style={{ left: 0, backgroundColor: "#EEEEEE" }}
></span>
</Tabs>
</Container>
</AppBar>
</Container>
);
};
If you don't want to explicitly check the paths then create an object that maps a pathname value to a tab "key" value, use the tab "key" value in each tab.

How can change the state of my link on click only if it’s not active already?

I have a sidebar with 2 links. I want when I press each one of them to get the active class so I can change the style of the active link, for example the Color of the text becomes blue, but my problem is that if I already am at the the first link and I click it again the state changes and so my style changes again without me changing the page. So if I am at the first link for example and I press it again, the active state goes on the second link while I still stay on the page of the first link.
Here is my code cause I can’t explain it really good :
const [active, setActive] = useState(false);
const setSideBar = () => setActive(!active);
return (
<Box className='menuWrapper'>
<Link to={PageRoutes.CHAPTER_ONE}>
<Box sx={{ display: 'flex' }}>
<Box
></Box>
<Box
onClick={setSideBar}
className={active ? 'menuItem' : ' menuItem active'}
>
<Box marginLeft='12px'>
<SideBarLineSvg />
</Box>
<span>CHAPTER ONE</span>
<h3>Control Programme</h3>
</Box>
</Box>
</Link>
<Link to={PageRoutes.CHAPTER_TWO}>
<Box sx={{ display: 'flex' }}>
<Box
></Box>
<Box
onClick={setSideBar}
className={active ? ' menuItem active' : 'menuItem'}
>
<Box marginLeft='12px'>
<SideBarLineSvg />
</Box>
<span>CHAPTER TWO</span>
<h3>National Programme</h3>
</Box>
</Box>
</Link>
</Box>
);
};
export default Menu;
In tried with if statement, but I guess I do something wrong because it doesn’t work at all when I add it
const [active, setActive] = useState(false);
const setSideBar = (key) => {
if(active && key===1){
setActive(!active);
}
if(!active && key===2)
setActive(!active);
}
}
return (
<Box className='menuWrapper'>
<Link to={PageRoutes.CHAPTER_ONE}>
<Box sx={{ display: 'flex' }}>
<Box
></Box>
<Box
onClick={()=>setSideBar(1)}
className={active ? 'menuItem' : ' menuItem active'}
>
<Box marginLeft='12px'>
<SideBarLineSvg />
</Box>
<span>CHAPTER ONE</span>
<h3>Control Programme</h3>
</Box>
</Box>
</Link>
<Link to={PageRoutes.CHAPTER_TWO}>
<Box sx={{ display: 'flex' }}>
<Box
></Box>
<Box
onClick={()=>setSideBar(2)}
className={active ? ' menuItem active' : 'menuItem'}
>
<Box marginLeft='12px'>
<SideBarLineSvg />
</Box>
<span>CHAPTER TWO</span>
<h3>National Programme</h3>
</Box>
</Box>
</Link>
</Box>
);
};
export default Menu;
send key value on onClickFn and do a execution as per key

Conditionally Rendering Components From An Array of Components

I have 3 cards I want to render on the screen all with a similar layout. What is this pattern called when we have a component as a value?
const steps = [
{
label: "Order details",
component: OrderDateStep,
},
{
label: "Driver details",
component: OrderDriverStep,
},
{
label: "Acknowledgements",
component: OrderAcknowledgementStep,
},
];
Additionally I keep running into an issue when these are conditionally rendered. I want to wait until stripe has initialised before displaying the form. However, I get an error Error: Rendered more hooks than during the previous render.. I know I can just add the different components but that isn't very scalable. Is there another way I can achieve this re-usable pattern without running into this issue with the number of hooks changing? Why does using step[x].component() change the number of hooks where just using the component does not?
{stripe && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}
If you want to make sure something is filled, or rendered, before displaying other data in react, you can just do
{
loadedVariable ?
<div>
......
</div>
:null
}
If your question is not fully answered by the point i get home i'll be happy to help you further.
Add one more conditionally into your render component to make sure that steps had filled:
{stripe && steps.length && (
<Elements
stripe={stripe}
options={{
clientSecret: paymentIntent?.client_secret,
}}
>
{steps.map((step, index) => {
return (
<Box
key={step.label}
sx={{
mt: 3,
}}
>
<Box sx={{ my: 2 }}>
<Typography variant="h5">{step.label}</Typography>
</Box>
{step.component()}
</Box>
);
})}
<Box sx={{ display: "flex", justifyContent: "end" }}>
<Button variant="contained" onClick={submitForm}>
Submit
</Button>
</Box>
</Elements>
)}

How can I automatically import an icon which fetches from the server in NextJS?

I want to dynamically generate a page in the Next-JS app. Inside this page should be imported automatically Icons which fetches from the server Instead of writing it statically:
{
"id": 1,
"title": "Budget",
"value": "$24K",
"persent" :"12%",
"duration" :"Since Last Month",
"icon":"MoneyIcon",
"rise":"false"
},
in this timeMoneyIcon from Material-UI.
In this case, was used map method in order to render fetched data.
How can I put this fetched icon name as a tag same as <MoneyIcon/> in the component?
{posts.map((post) => (
<>
<Grid item lg={3} sm={6} xl={3} xs={12}>
<Card sx={{ height: "100%" }}>
<CardContent>
<Grid container spacing={3} sx={{ justifyContent: "space-between" }}>
<Grid item>
<Typography color="textSecondary" gutterBottom variant="overline">
{post.title}
</Typography>
<Typography color="textPrimary" variant="h4">
{post.value}
</Typography>
</Grid>
<Grid item>
<Avatar
sx={{
backgroundColor: "error.main",
height: 56,
width: 56,
}}
>
**<MoneyIcon />**
</Avatar>
</Grid>
</Grid>
<Box
sx={{
pt: 2,
display: "flex",
alignItems: "center",
}}
>
<ArrowDownwardIcon color="error" />
<Typography
color="error"
sx={{
mr: 1,
}}
variant="body2"
>
{post.persent}
</Typography>
<Typography color="textSecondary" variant="caption">
{post.duration}
</Typography>
</Box>
</CardContent>
</Card>
</Grid>
</>
))}
If I understand you correctly you are trying to pass the MUI Icon to the Component and render the icon. For that you can simply pass the Icon as a value of your object wrapped in a React Fragment.
import AccountBalanceIcon from '#mui/icons-material/AccountBalance';
const myObject = {
id: 1,
value: "24K",
icon: <><AccountBalanceIcon/></>
)};
export default function MyComponent(props) {
return(
<div>
<span>{myObject.value}</span>
{myObject.icon}
<div>
)
}
You can import them dynamically by name for example you can fetch the Icon name from server then in the page do this:
import * as MuiIcons from '#mui/icons-material'
function YourPage({IconName}){
const IconComponent = MuiIcons[IconName]
return <Grid container>
<IconComponent />
</Grid>
}
and if you want for example #mui/icons-material/Memory the Icon name is "Memory".
But I think this approach is not treeshakable you may bring the entire jungle , I am not sure about that though, you should check the bundle after compiling.

Grid properties on second child element of Accordion not working

Here I have defined the parent element display:"Grid" and with my child element have defined the gridColumnStart as below. It's working fine with the first element but not with the second child element. Below have attached my code.
return (
<Accordion
sx={{
display: "grid",
gridTemplateColumns: "1fr 2fr 1fr"
}}
>
<AccordionSummary
sx={{
gridColumnStart: "2",
gridColumnEnd: "4"
}}
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<div>
<Typography variant="h1" sx={{ fontSize: "40px" }}>
{props.Title}
</Typography>
<Typography variant="subtitle1">{props.Description}</Typography>
<Button
size="small"
variant="outlined"
sx={{
width: "140px",
textAlign: "center",
marginTop: "10px",
color: "#2196F3",
borderColor: "#2196F3"
}}
>
More details
</Button>
</div>
</AccordionSummary>
<AccordionDetails
sx={{
gridColumnStart: "2",
gridColumnEnd: "3"
}}
>
{props.children}
</AccordionDetails>
</Accordion>
);
So here gridColumnStart and End working with AccordionSummary but not with AccordionDetails.
This is how it looks but I wanted the text to be in the middle. Not sure if the approach I'm heading towards is correct. Would appreciate if someone could help me.
This is a simplified Accordion definition:
<AccordionRoot>
<AccordionSummary />
<TransitionComponent>
<div>
<AccordionDetail />
</div>
</TransitionComponent>
</AccordionRoot>
The reason your AccordionDetail styles doesn't work is because it's applied to the children of the grid item instead of the grid item itself. To fix it, you need to set the grid properties directly in the child of the grid container which is TransitionComponent, if you inspect the element you can see the class name of the DOM elelment is MuiCollapse-root:
<Accordion
sx={{
display: "grid",
gridTemplateColumns: "1fr 2fr 1fr",
"& .MuiCollapse-root": {
gridColumnStart: "2",
gridColumnEnd: "3"
}
}}
>

Resources