This is either very simple or I am doing it completely wrong. I am a novice so please advise.
I am trying to show different components inside different tabs using Material UI using array map. The tabs are showing fine but the components do not render. Basically if the array label is 'Welcome', the tab name should be 'Welcome' and the Welcome component should show up and so on. Please help!
return (
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} aria-label="basic tabs example">
{fetchedCategories.map((category) => (
<Tab key={category.label} label={category.label} />
))}
</Tabs>
</Box>
{fetchedCategories.map((category, index) => {
const Component=myComponents[category.label];
})}
{fetchedCategories.map((category, index) => (
<TabPanel key={category.label} value={value} index={index}>
<Component label={category.label} />
</TabPanel>
))}
</Box>
);
Here is my props & Component function:
interface ComponentProps {
label: string;
value?: number;
}
function Component (props: ComponentProps)
{
const {label, value} = props;
return myComponents[label];
}
const myComponents = {
'Welcome': Welcome,
'Salad/Soup': Welcome
}
Try something like:
function Component({ label, value }: ComponentProps) {
const [Comp, setComponent] = useState(<div />);
React.useEffect(() => {
const LabelComp = myComponents[label];
if (label && LabelComp) {
setComponent(<LabelComp value={value} />); // <-- if you want to pass value to you component
}
}, [value, label]);
return Comp;
}
And you will use it like:
const App = () => {
return <Component label={"myComponentLabel"} value={"some value"} />;
};
Related
I have written page that uses Tabs and Tab Panel. I'm writing UI tests using react-testing library. The theme has been modified into custom theme that is imported at the root of the Next.js app. I'm using page that uses a component with form for the Tab. In the listening it's <MyComponent />. The component inside has Select component used from ChakraUI. Other inputs don't affect on the error that appears.
Libraries:
ChakraUI 2.8
react-hook-form
Next.js 12
The error that appears is
Cannot read properties of undefined (reading '_focus')
TypeError: Cannot read properties of undefined (reading '_focus')
at /path/to/project/node_modules/#chakra-ui/select/dist/index.cjs.js:102:22
My Page in a nutchell looks like
const MyPage () => {
const instrumentInfoTab = React.useRef() as React.MutableRefObject<HTMLInputElement>;
return (
<Tabs>
<TabList><Tab>...</Tab</TabLists>
</Tabs>
<TabPanels>
<TabPanel>
<MyComponent updateTransactionState={/*some function to handle state*/} nextTabRef={instrumentInfoTab} />
</TabPanel>
</TabPanels>
<Tab
)
}
MyComponent
interface IInstrumentInfoPanelProps {
nextTabRef: React.MutableRefObject<HTMLInputElement>;
updateTransactionState: (data: InstrumentInfoInput) => void;
}
const MyComponent = (props: IInstrumentInfoPanelProps) => {
const { nextTabRef, updateTransactionState } = props;
const textColor = useColorModeValue('secondaryGray.900', 'white');
const methods = useForm<InstrumentInfoInput>({
resolver: zodResolver(instrumentInfoSchema)
});
const { handleSubmit, register } = methods;
const onSubmit = (data: InstrumentInfoInput) => {
updateTransactionState(data);
nextTabRef.current.click();
};
return (
<TabPanel>
<CustomCard>
<Text>
Instrument Info
</Text>
<Flex>
<form onSubmit={handleSubmit(onSubmit)}>
<FormProvider {...methods}>
<SimpleGrid>
<Stack>
<Flex direction="column" mb="34px">
<FormLabel
ms="10px"
htmlFor="transactionType"
fontSize="sm"
color={textColor}
fontWeight="bold"
_hover={{ cursor: 'pointer' }}>
Transaction type*
</FormLabel>
<Select
{...register('transactionType')}
id="transactionType"
variant="main"
defaultValue="buy">
<option value="buy">BUY</option>
<option value="sell">SELL</option>
</Select>
</Flex>
</Stack>
<Stack>
<InputField
id="accountName"
name="accountName"
placeholder="eg. Coinbase"
label="Account Name*"
data-testid="instrumentInfoPanel-accountName"
/>
</Stack>
</SimpleGrid>
<Flex justify="space-between" mt="24px">
<Button
type="submit">
Next
</Button>
</Flex>
</FormProvider>
</form>
</Flex>
</CustomCard>
</TabPanel>
);
};
export default MyComponent;
My Test looks like. The error appears in render function
jest.mock('next/link', () => {
return ({ children }) => {
return children;
};
});
interface IWrapedComponent {
children?: JSX.Element;
}
const test = (ref: MutableRefObject<HTMLElement | null>): void => {
if (ref.current) ref.current.focus();
};
const WrappedComponent = (props: IWrapedComponent) => {
const { children } = props;
const ref = React.useRef() as React.MutableRefObject<HTMLInputElement>;
useEffect(() => {
test(ref);
}, []);
return (
<Tabs>
<TabList>
<Tab ref={ref}></Tab>
</TabList>
<TabPanels>
<MyComponent />
</TabPanels>
</Tabs>
);
};
describe('Instrument Info Panel', () => {
it('should render inputs for instrument info', () => {
render(
<QueryClientProvider client={new QueryClient()}>
<WrappedComponent />
</QueryClientProvider>
);
});
});
In order to debug an issue I have tried to remove other inputs from form and it worked when there were different input types than Select from ChakraUI.
I'm working on react library to create to display list of events dynamically with react material ui tab. Im' getting following error. How I can check following props is not undefined before render Tab component.
Eg:
{Object.keys(props.schedule).map((name: string, index: number) => {
return <Tab label={name} {...a11yProps(index)} />;
})}
Error:
Uncaught TypeError: Cannot convert undefined or null to object
The above error occurred in the <BasicTabs> component:
channelDetail.tsx
function ChannelDetail() {
const { channelId } = useParams();
const dispatch = useAppDispatch();
const { schedule }: Channels = useAppSelector(selectChannelById); // please check sample response
useEffect(() => {
dispatch(getChannelById(channelId));
}, [dispatch, channelId]);
return(
<ChannelTab schedule={schedule} />
)
}
channelTab.tsx
export default function BasicTabs(props: any) {
const [value, setValue] = React.useState(0);
const handleChange = (event: React.SyntheticEvent, newValue: number) => {
setValue(newValue);
};
return (
<Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={value}
onChange={handleChange}>
{Object?.keys(props?.schedule).map((name: string, index: number) => {
return <Tab label={name} {...a11yProps(index)} />;
})}
</Tabs>
</Box>
<TabPanel value={value} index={0}>
Item One
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Three
</TabPanel>
</Box>
);
}
Sample response for schedule
const schedule = {
'today': [
{ title: "Harry Potter", time: "2pm" },
{ title: "Rampage", time: "4pm" }
],
'tommorow': [
{ title: "Die Hard", time: "3pm" },
{ title: "Rambo", time: "6pm" }
]
};
I need to create bunch of Tab nodes in a Tabs. I thought that map a array would be easier to manage it. But I was kind of don't know how to make it works with MATERIAL UI Taps components.
My target is when I click the tab, the TabPanel supposed to show the correct components pending on the index.
The Tabs part works just fine, and it will be siwtch components properly if I keep the TabPanel one by one. But it won't be work if I map the array to create the TabPanel.
Please advise how to fix it.
//TODO set the router for each tab, wondering if it could be done in an array and map it
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp:<LawyerPurchase />
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp:<Refinance />
},
// {},
]
function TabPanel(props) {
const { children, value, index } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`wrapped-tabpanel-${index}`}
>
{value === index && (
<Box p={3}>
<Typography component={'div'}>{children}</Typography>
</Box>
)}
</div>
);
}
// TabPanel.propTypes = {
// // children: PropTypes.node,
// index: PropTypes.any,
// value: PropTypes.any,
// };
const useStyles = makeStyles((theme) => ({
root: {
backgroundColor: theme.palette.background.paper,
},
item: {
minWidth: '0px'
}
}));
export default function TabsWrappedLabel() {
const classes = useStyles();
const [value, setValue] = React.useState(false);
const updateNotes = useContext(NotesUpdate);
const history = useHistory();
const handleChange = (event, newValue) => {
setValue(newValue);
};
const clean_notes_push = (item) => {
//comments
updateNotes.setCondition('');
updateNotes.setFunNotes('');
updateNotes.setBusNotes('');
history.push(item.path);
}
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs
value={value}
onChange={handleChange}
variant='fullWidth'
TabIndicatorProps={{ style: { background: '#00ff33' } }}
>
{tab_item.map((item) => ( // The Tab works fine here.
<Tab
wrapped
key={item.index}
index={item.index}
label={item.label}
onClick={() => (clean_notes_push(item))}
/>
))}
</Tabs>
</AppBar>
{/* <TabPanel value={value} index={0}> // It works if I put the TabPanel one by one, but I'm trying to map the tab_item array to generate them, problem is I don't know how to make it works.
<LawyerPurchase/>
</TabPanel>
<TabPanel value={value} index={1}>
Item Two
</TabPanel>
<TabPanel value={value} index={2}>
Item Two
</TabPanel>
*/}
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp}
</TabPanel>
))}
</div>
);
}
You may need to update the tab_item object by:
//TODO Declare the function to render the component in a tab pane
const tab_item = [
{
index: 1,
label: 'Purchase',
path: '/Linx_Homeline/Purchase',
tabPanel_comp: () => <LawyerPurchase /> // function returns the component
},
{
index: 2,
label: 'Refinance',
path: '/Linx_Homeline/Refinance',
tabPanel_comp: () => <Refinance />
},
]
And replace the TabPanel render map function by:
{tab_item.map((item) => ( // Not working here, not even generate a TabPanel
<TabPanel
key={item.index}
value={value}
index={1}
>
{item.tabPanel_comp()} //calls the function to render the component
</TabPanel>
))}
I'm using Material UI's nested/select ItemList component to dynamically generates any number of dropdown menu items based on how many items belong in this header as you can maybe tell from the mapping function. On another file 1 layer above this one, I am again mapping and generating multiple of these DropDownMenus, is it possible for these components to communicate to each other?
This is the file in question
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
maxWidth: 330,
backgroundColor: theme.palette.background.paper,
},
nested: {
paddingLeft: theme.spacing(4),
}
}));
export default function DropDownMenu(props) {
const classes = useStyles();
const [open, setOpen] = React.useState(true);
let unitName = props.unit[0];
let chapterList = props.unit.slice(1);
const [selectedIndex, setSelectedIndex] = React.useState(1);
const handleListItemClick = (index) => {
console.log("ItemClicked");
console.log(index);
setSelectedIndex(index);
};
const handleClick = () => {
setOpen(!open);
};
const selectMenuItem = (chapter, index) => {
props.chooseChapter(chapter)
handleListItemClick(index)
}
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem button
className={classes.selected}
selected={selectedIndex === index}
onClick={() => selectMenuItem(chapter, index)}
key={index}>
<ListItemText primary={chapter} />
</ListItem>
)
})
return (
<List
component="nav"
aria-labelledby="nested-list-subheader"
subheader={
<ListSubheader component="div" id="nested-list-subheader">
</ListSubheader>
}
className={classes.root}
>
<ListItem button onClick={handleClick}>
<ListItemText primary={unitName} />
{!open ? <ExpandLess /> : <ExpandMore />}
</ListItem>
<Collapse in={!open} timeout="auto" unmountOnExit>
<List component="div" disablePadding className={classes.selected}>
{dropDownUnit}
</List>
</Collapse>
</List>
);
}
Psudo Style - What I'm trying to accomplish
<DropDownMenu>
<MenuItem> // Suppose this is selected
<MenuItem>
<DropDownMenu>
<MenuItem> // onClick --> Select this and deselect all other selected buttons
You can have a parent of these components such that the parent will keep the state of who is active. That way you can pass that state & the state setter as props so that everyone will know who is active
export default function App() {
const [selectedItem, setSelectedItem] = React.useState();
return (
<>
<DropDownMenu
selectedItem={selectedItem} // pass down as props
setSelectedItem={setSelectedItem} // pass down as props
unit={...}
chooseChapter={function () {}}
/>
...
On the children, simply refactor the Call To Action (in this case onClick) to set the state using the passed down props. Pay attention to the selected prop of ListItem, we now use the state we have passed down from the parent
let dropDownUnit = chapterList.map((chapter, index) => {
return (
<ListItem
button
className={classes.selected}
selected={props.selectedItem === chapter}
onClick={() => props.setSelectedItem(chapter)}
key={index}
>
<ListItemText primary={chapter} />
</ListItem>
);
});
What i'm trying to achieve is to have a BaseComponent which will be reuse in different ParentComponent.
My base card component props is ->
export type MSGameCardProps = {
title: string;
fetchGamesFn : (searchText: string) => Promise<IResultObject<any,any>>;
};
My base card render all the necessary basic logics and controls (inputs,autocomplete,title).
For example, it provide an autocomplete which have a simple search debounce functionality.
Me parent component will not necessary have props and will use the base card like so :
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.FC<MSGameSrcCardProps> = () => {
const gameSvc = useGameService();
const fetchGame = async (searchText: string) => {
const rs = await result(gameSvc.getAll(searchText));
return rs;
};
return (
<MSGameCard title={"Convert From"} fetchGamesFn={fetchGame}></MSGameCard>
);
};
export default MSGameSrcCard;
The parent component will provide a fetchGames function which can be different.
It will also set the title and may later on set some other flags.
This pattern result with this error : Type '{}' is missing the following properties from type 'MSGameCardProps': title, fetchGamesFn when trying to use the parent component in my page like so : <MSGameSrcCard></MSGameSrcCard>
I don't understand why my parent should have those properties since they are only required in the child component and are fullfill in my parent component function.
I don't want to make them optional(?) since they are actually required; of course only for my base component
I did try to export my basecomponent AS ANY which remove the error but now my props.fetchGamesFn is always undefined even passing it in inside my parent component function.
Maybe i'm doing it wrong but is there a way to have a parent components with no props with child that required props?
EDIT : Here is my MSGameCard base component definition
export const MSGameCard: React.FC<MSGameCardProps> = props => {
const [games, setGames] = React.useState([
{
name: ""
}
]);
const [selectedGame, setSelectedGame] = React.useState<any>();
const [previousGame, setPreviousGame] = React.useState<any>();
const [isLoading, setIsLoading] = React.useState(false);
const [opacity, setOpacity] = React.useState(0);
const fetchGameBase = (searchText: string) => {
setIsLoading(true);
console.log(props.fetchGamesFn);
props.fetchGamesFn(searchText).then(rs =>{
if (rs.isSuccess) setGames(rs.result.data);
setIsLoading(false);
})
};
const searchDebounce = debounce(300, fetchGameBase);
React.useEffect(() => {
fetchGameBase("");
}, []);
const onGameChanged = (event: any, value: any) => {
if (selectedGame) setPreviousGame(selectedGame);
setOpacity(0);
if (value) {
setTimeout(() => {
setSelectedGame(value);
setOpacity(0.2);
}, 300);
}
};
const onInputChanged = (e: any) => {
let value = e.target.value;
if (!value) value = "";
searchDebounce(value);
};
const getSelectedGameImg = () => {
const bgUrl: string = selectedGame
? selectedGame.bg_url
: previousGame?.bg_url;
return bgUrl;
};
return (
<Card style={{ position: "relative", zIndex: 1 }} variant="outlined">
<CardContent style={{ zIndex: 1 }}>
<Typography variant="h5" gutterBottom>
{props.title}
</Typography>
<Grid container spacing={3}>
<Grid item xs={12} sm={6}>
<Autocomplete
options={games}
getOptionLabel={option => option.name}
onChange={onGameChanged}
onInputChange={onInputChanged}
renderInput={params => (
<TextField
{...params}
label="Source game"
fullWidth
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{isLoading ? (
<CircularProgress color="primary" size={30} />
) : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
/>
</Grid>
</Grid>
<Grid container direction="row" spacing={3}>
<Grid item xs={12} sm={6}>
<TextField label="DPI" fullWidth />
</Grid>
<Grid item xs={12} sm={6}>
<TextField label="Sensitivity" fullWidth />
</Grid>
</Grid>
</CardContent>
<img
style={{ opacity: opacity }}
className="gameImage"
src={getSelectedGameImg()}
/>
</Card>
);
};
export default MSGameCard;
Keep updating notice
After checked the minimum reproducible example you have provided.
I found no type error, am I missing something?
Since the error occurred in both two props, I would leave only the string for the check
import * as React from "react";
import "./styles.css";
export type MSGameCardProps = {
title: string;
};
export type MSGameSrcCardProps = {};
export const MSGameSrcCard: React.SFC<MSGameSrcCardProps> = () => {
return <MSGameCard title={"Convert From"} />;
};
const MSGameCard: React.SFC<MSGameCardProps> = (props: MSGameCardProps) => {
console.log(props); // Object {title: "Convert From"}
return <></>;
};
export default function App() {
return (
<div className="App">
<MSGameSrcCard />
</div>
);
}
Try it online here: