react-router+antD/ How to highlight a menu item when press back/forward button? - react-router

I create a menu and want to highlight the item which i choose,and i did it. But when i press back/forward button,the menu item don't highlight. What should i do?
I have tried to use addEventListener but failed.
Have someone could give some advice?
class Sidebar extends React.Component {
constructor(props) {
super(props);
this.state={
test: "home"
}
this.menuClickHandle = this.menuClickHandle.bind(this);
}
componentWillMount(){
hashHistory.listen((event)=>{
test1 = event.pathname.split("/");
});
this.setState({
test:test1[1]
});
}
menuClickHandle(item) {
this.props.clickItem(item.key);
}
onCollapseChange() {
this.props.toggle();
}
render() {
var {collapse} = this.props;
return (
<aside className="ant-layout-sider">
<Menu mode="inline" theme="dark" defaultSelectedKeys={[this.state.test || "home"]} onClick={this.menuClickHandle.bind(this)}>
<Menu.Item key="home">
<Link to="/home">
<Icon type="user"/><span className="nav-text">用户管理</span>
</Link>
</Menu.Item>
<Menu.Item key="banner">
<Link to="/banner">
<Icon type="setting"/><span className="nav-text">Banner管理</span>
</Link>
</Menu.Item>
</Menu>
<div className="ant-aside-action" onClick={this.onCollapseChange.bind(this)}>
{collapse ? <Icon type="right"/> : <Icon type="left"/>}
</div>
</aside>
)
}
}

I could come up with a solution using WithRouter
import React,{ Component } from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import { Layout, Menu, Icon } from 'antd';
import PropTypes from 'prop-types';
const { Sider } = Layout;
class SideMenu extends Component{
static propTypes = {
location: PropTypes.object.isRequired
}
render() {
const { location } = this.props;
return (
<Sider
trigger={null}
collapsible
collapsed={this.props.collapsed}>
<div className="logo" />
<Menu
theme="dark"
mode="inline"
defaultSelectedKeys={['/']}
selectedKeys={[location.pathname]}>
<Menu.Item key="/">
<NavLink to="/">
<Icon type="home" />
<span>Home</span>
</NavLink>
</Menu.Item>
<Menu.Item key="/other">
<NavLink to="/other">
<Icon type="mobile"/>
<span>Applications</span>
</NavLink>
</Menu.Item>
<Menu.Item key="/notifications">
<NavLink to="/notifications">
<Icon type="notification" />
<span>Notifications</span>
</NavLink>
</Menu.Item>
</Menu>
</Sider>
)
}
}
export default withRouter(SideMenu);

Intercepts the current URL and then set selectedKeys(Note that it is not defaultSelectedKeys).
componentWillMount(){
hashHistory.listen((event)=>{
pathname = event.pathname.split("/");
if(pathname != null){
this.setState({
test:pathname[1]
});
}
});
}

you can set the paths of the link as keys on each Menu.Item . then selectedKeys={this.props.location.pathname}
<Menu
theme="light"
mode='inline'
selectedKeys={[this.props.location.pathname,]}
>
<Menu.Item key={item.path} style={{float:'right'}}>
Link to={item.path}>{item.name}</Link>
</Menu.Item>
{menulist}
</Menu>
Item would be set active according to the current path.
i added [] and trailing comma because selectedKeys accepts array while this.props.location.pathname is a String. i just code as hobby so idont know if its acceptable.

The following answer assumes you are using hooks. I know you are not in your question, but it might be useful for other people. In addition, this solution will work if you have nested paths such as /banner/this/is/nested, and it works not only when pressing back and forward buttons but also when refreshing the current page:
import React, { useState, useEffect } from 'react'
import { useHistory, useLocation } from 'react-router-dom'
import { Layout, Menu } from 'antd'
const { Sider } = Layout
const items = [
{ key: '1', label: 'Invoices', path: '/admin/invoices' },
{ key: '2', label: 'Service Details', path: '/admin/service-details' },
{ key: '3', label: 'Service Contract Details', path: '/admin/service-contract-details' },
{ key: '4', label: 'Cost Centers', path: '/admin/cost-centers' },
{ key: '5', label: 'Clients', path: '/admin/clients' },
{ key: '6', label: 'Vendors', path: '/admin/vendors' }
]
const Sidebar = () => {
const location = useLocation()
const history = useHistory()
const [selectedKey, setSelectedKey] = useState(items.find(_item => location.pathname.startsWith(_item.path)).key)
const onClickMenu = (item) => {
const clicked = items.find(_item => _item.key === item.key)
history.push(clicked.path)
}
useEffect(() => {
setSelectedKey(items.find(_item => location.pathname.startsWith(_item.path)).key)
}, [location])
return (
<Sider style={{ backgroundColor: 'white' }}>
<h3 style={{ paddingLeft: '1rem', paddingTop: '1rem', fontSize: '1.25rem', fontWeight: 'bold', minHeight: 64, margin: 0 }}>
Costek
</h3>
<Menu selectedKeys={[selectedKey]} mode='inline' onClick={onClickMenu}>
{items.map((item) => (
<Menu.Item key={item.key}>{item.label}</Menu.Item>
))}
</Menu>
</Sider>
)
}
export default Sidebar
This is how the sidebar will look like:

#Nadun's solution works for paths that don't contains arguments. If you're however using arguments in your routes, like me, here's a solution that should work for any route path, including /users/:id or crazy stuff like /users/:id/whatever/:otherId. It uses react-router's matchPath API, which uses the exact same logic as the Router component.
// file with routes
export const ROUTE_KEYS = {
ROOT: "/",
USER_DETAIL: "/users/:id",
};
export const ROUTES = {
ROOT: {
component: Home,
exact: true,
key: ROUTE_KEYS.ROOT,
path: ROUTE_KEYS.ROOT,
},
USER_DETAIL: {
component: Users,
key: ROUTE_KEYS.USER_DETAIL,
path: ROUTE_KEYS.USER_DETAIL,
},
};
.
// place within the App component
<Router>
<Layout>
<MyMenu />
<Layout>
<Layout.Content>
{Object.values(ROUTES).map((route) => (
<Route {...route} />
))}
</Layout.Content>
</Layout>
</Layout>
</Router>
.
// MyMenu component
const getMatchedKey = (location) =>
(
Object.values(ROUTES).find((route) =>
matchPath(location.pathname, route)
) || {}
).path;
const MyMenu = ({ location }) => {
return (
<Layout.Sider>
<AntMenu mode="inline" selectedKeys={[getMatchedKey(location)]}>
<AntMenu.SubMenu
title={
<React.Fragment>
<Icon type="appstore" />
Home
</React.Fragment>
}
>
<AntMenu.Item key={ROUTE_KEYS.ROOT}>
<Icon type="appstore" />
<span className="nav-text">
Some subitem
</span>
</AntMenu.Item>
</AntMenu.SubMenu>
<AntMenu.SubMenu
title={
<React.Fragment>
<Icon type="user" />
Users
</React.Fragment>
}
>
<AntMenu.Item key={ROUTE_KEYS.USER_DETAIL}>
<Icon type="user" />
<span className="nav-text">
User detail
</span>
</AntMenu.Item>
</AntMenu.SubMenu>
</AntMenu>
</Layout.Sider>
);
};
export default withRouter(MyMenu);

I do something like this but it doesn't seem to be reactive. Like if I navigate to a new page through a button (not from the menu items), it will not update the active link until the page refreshes.
import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';
const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;
const { appName } = appConfig;
const AppNavigation = React.createClass({
getInitialState() {
return {
current: this.props.pathname
};
},
handleClick(e) {
browserHistory.push(e.key);
this.setState({ current: e.key });
return;
},
render() {
return (
<Row className='landing-menu' type="flex" justify="space-around" align="middle" style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
<Col span='19'>
<Link to='/'>
<h2 style={{fontSize: 21, color: '#fff'}}>
{appName}
<Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
</h2>
</Link>
</Col>
<Col span='5'>
<Menu onClick={this.handleClick} selectedKeys={[this.state.current]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
<Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
<Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
<Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
</Menu>
</Col>
</Row>
);
},
});
export const App = React.createClass({
propTypes: {
children: React.PropTypes.element.isRequired,
},
componentWillMount(){
if (Meteor.userId()) {
browserHistory.push('/student/home')
}
},
render() {
return (
<div style={{position: 'relative'}}>
<AppNavigation pathname={this.props.location.pathname} />
<div style={{minHeight: '100vh'}}>
{ this.props.children }
</div>
</div>
);
}
});
EDIT:
the below works pretty well. pass down the pathname from react-router and pop that as a prop into selectedKeys
import React from 'react';
import { StyleSheet, css } from 'aphrodite'
import { browserHistory, Link } from 'react-router';
import 'antd/lib/menu/style/css';
import 'antd/lib/icon/style/css';
import 'antd/lib/row/style/css';
import 'antd/lib/col/style/css';
import 'antd/lib/message/style/css';
import { appConfig } from '../../modules/config';
import { Menu, Icon, Row, Col, message } from 'antd';
const SubMenu = Menu.SubMenu;
const MenuItemGroup = Menu.ItemGroup;
const { appName } = appConfig;
const AppNavigation = React.createClass({
getInitialState() {
return {
current: this.props.pathname
};
},
handleClick(e) {
browserHistory.push(e.key);
this.setState({ current: e.key });
return;
},
render() {
return (
<Row className='landing-menu' type="flex" justify="space-around" align="middle" style={{height: 55, zIndex: 1000, paddingLeft: 95, color: '#fff', backgroundColor: '#da5347', borderBottom: '1px solid #e9e9e9'}}>
<Col span='19'>
<Link to='/'>
<h2 style={{fontSize: 21, color: '#fff'}}>
{appName}
<Icon type="rocket" color="#fff" style={{fontWeight: 200, fontSize: 26, marginLeft: 5 }}/>
</h2>
</Link>
</Col>
<Col span='5'>
<Menu onClick={this.handleClick} selectedKeys={[this.props.pathname]} mode="horizontal" style={{height: 54, backgroundColor: '#da5347', borderBottom: '0px solid transparent'}}>
<Menu.Item style={{height: 54, }} key="/">Home</Menu.Item>
<Menu.Item style={{height: 54, }} key="/signup">Signup</Menu.Item>
<Menu.Item style={{height: 54, }} key="/login">Login</Menu.Item>
</Menu>
</Col>
</Row>
);
},
});
export const App = React.createClass({
propTypes: {
children: React.PropTypes.element.isRequired,
},
componentWillMount(){
if (Meteor.userId()) {
browserHistory.push('/student/home')
}
},
render() {
return (
<div style={{position: 'relative'}}>
<AppNavigation pathname={this.props.location.pathname} />
<div style={{minHeight: '100vh'}}>
{ this.props.children }
</div>
</div>
);
}
});

If you are using an array and mapping over it (as in my case) to set menu Items, They must be in the same order as they appear in the Side menu otherwise, an active bar or background will not be shown.

Environment: React Router V5, Ant Design V4.17.0
I solved this issues by override the onClick props of Menu.Item of antd
<Menu theme="light" mode="inline">
{menuItems.map((item) => {
return (
<NavLink
to={item.navigation}
component={({ navigate, ...rest }) => <Menu.Item {...rest} onClick={navigate} />}
key={item.key}
activeClassName="ant-menu-item-selected"
>
{item.icons}
<span>{item.name}</span>
</NavLink>
)
}
)}
</Menu>
The NavLink component will pass navigate prop to Menu.Item, we need to map it to onClick prop and click behaviour will work correctly.

Related

"temporary" variant not showing in Drawer component Material-ui

I am trying to make the drawer component on material-ui a responsive drawer, by making it to be toggled open and close on smaller screens and permanent on desktop
I followed the documentation on mui, which says to add the temporary variant to the drawer, however it doesn't show up at all on desktop mode
Below is my drawer component
import React from "react";
import ReactDOM from "react-dom";
import { useHistory } from "react-router-dom";
import { makeStyles, Typography, Drawer, Toolbar, Divider, List, ListItem, ListItemText, ListItemIcon, Container, Box } from "#material-ui/core";
import HomeOutlinedIcon from '#material-ui/icons/HomeOutlined';
import AlternateEmailOutlinedIcon from '#material-ui/icons/AlternateEmailOutlined';
import NotificationsNoneOutlinedIcon from '#material-ui/icons/NotificationsNoneOutlined';
import DashboardOutlinedIcon from '#material-ui/icons/DashboardOutlined';
import ArchiveOutlinedIcon from '#material-ui/icons/ArchiveOutlined';
const drawerWidth = 280
const useStyles = makeStyles((theme) => ({
drawer : {
width: drawerWidth,
},
drawerPaper : {
width: drawerWidth,
},
list : {
marginLeft: '24px',
marginRight: '24px'
},
listItem : {
padding: '8px',
borderRadius: '5px',
},
text : {
color: theme.palette.primary.light,
fontWeight: '500',
lineHeight: '2000'
},
title : {
color: theme.palette.primary.light,
textAlign: 'center',
display: 'block',
fontWeight: '500',
fontSize: '20px',
marginTop: '10px',
marginBottom: '10px'
}
}));
export default function LeftBar(){
const classes = useStyles();
const [mobileOpen, setMobileOpen] = React.useState(false);
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const list = [
{
text : 'Home',
icon : <HomeOutlinedIcon />,
path : '/'
},
{
text : 'Personal',
icon : <AlternateEmailOutlinedIcon />,
path : '/'
},
{
text : 'Notificatons',
icon : <NotificationsNoneOutlinedIcon />,
path : '/'
},
{
text : 'Dashboard',
icon : <DashboardOutlinedIcon />,
path : '/'
},
{
text : 'Archives',
icon : <ArchiveOutlinedIcon />,
path : '/'
}
];
return (
<Box>
<Drawer
className={classes.drawer}
anchor="left" classes={{
paper: classes.drawerPaper
}}
open={mobileOpen}
onClose={handleDrawerToggle}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
sx={{
display: { xs: 'block', sm: 'none' },
flexShrink: 0,
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
<Toolbar />
<Container>
<Typography variant="h5" component="h2" className={classes.title} gutterBottom>
My Account
</Typography>
</Container>
<Divider />
<List className={classes.list}>
{ list.map((item) => (
<ListItem key={item.text} className={classes.listItem} button>
<ListItemIcon className={classes.icon}>
{item.icon}
</ListItemIcon>
<ListItemText sx={{color: 'primary'}} primary={item.text} className={classes.text}/>
</ListItem>
))}
</List>
</Drawer>
</Box>
);
};
You have only included the drawer for xs - with display: { xs: 'block', sm: 'none' } that drawer will not display for any size but xs. You will need another drawer for sm and up. The MUI docs show a second drawer like this:
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{you would put your list stuff here}
</Drawer>
You will probably need to move the Toolbar component outside of the Drawer component.

Call function to update Context in React Native

I am having problems calling a function in React Native. I simply want to change the value of 'Context'. Here is some code, first the script for 'context':
//LogContext.js
import React, { useState } from 'react'
export const LogContext = React.createContext({
set: "en",
login: "false"
})
export const LogContextProvider = (props) => {
const setLog = (login) => {
setState({set: "jp", login: login})
}
const initState = {
set: "en",
login: "false"
}
const [state, setState] = useState(initState)
return (
<LogContext.Provider value={state}>
{props.children}
</LogContext.Provider>
)
}
and the 'app.js' code:
//app.js
import React, { useState, useContext } from 'react';
import { Button, Text, TextInput, View } from 'react-native';
import { NavigationContainer } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
import { LogContextProvider, LogContext } from './LogContext'
function HomeScreen({ navigation }) {
const state = useContext(LogContext);
return (
<>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Passed config: {JSON.stringify({state})}</Text>
<Text>Home Screen</Text>
</View>
{state.login === 'false' ? (
<Button
title="Go to Login"
onPress={() => navigation.navigate('Login')}
/>
) : (
<Button title="Stuff" onPress={() => navigation.navigate('DoStuff')} />
)}
</>
);
}
function LoginScreen({ navigation }) {
const state = useContext(LogContext);
//do stuff to login here...
state.setLog('true'); //not functional...
return (
<LogContext.Provider value={'true'}> //value={'true'} also not functional...
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Login Screen</Text>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
</View>
</LogContext.Provider>
);
}
function StuffScreen({ navigation }) {
//do other stuff here...
}
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="DoStuff" component={StuffScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default App;
Obviously I am not too familiar with React Native. Any advice on how to call the "setLog()" function as to enable an update of the value for the 'Context' global variable would be greatly appreciated. I thank you in advance.
I am trying to modify my "App()" function to wrap the Navigator within the provider as suggested by another user...however this following is completely non-functional...suggestions appreciated:
const Stack = createStackNavigator();
function App() {
const [data, setData] = useState({
set: 'en',
login: 'false',
});
const state = { data, setData };
return (
<LogContext.Provider value={state}>
<NavigationContainer>
{state.data.login === 'true' ? (
<Stack.Navigator>
<Stack.Screen name="BroadCast" component={VideoScreen} />
<Stack.Screen name="Logout" component={LogoutScreen} />
</Stack.Navigator>
) : (
<Stack.Navigator>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
)}
</NavigationContainer>
</LogContext.Provider>
);
}
The issue you are having is not having a set function in your context and i dont see a need for a separate LogContext provider function.
You can simply do that part in your app.js or whatever the root function. The below example does that. You can see how a state value is passed along with a function to set the values and this can be modified from teh Login component which is inside the provider. If you use a separate provider its a bit confusing. The below is a working example without the navigation part to give you an idea.
const LogContext = createContext({
data: {
set: 'en',
login: 'false',
},
});
export default function App() {
const [data, setData] = useState({
set: 'en',
login: 'false',
});
const state = { data, setData };
return (
<LogContext.Provider value={state}>
<View style={{ flex: 1 }}>
<Text>{JSON.stringify(state.data)}</Text>
<Login />
</View>
</LogContext.Provider>
);
}
const Login = () => {
const state = React.useContext(LogContext);
return (
<View>
<Button
onPress={() => state.setData({ set: 'bb', login: 'true' })}
title="Update"
/>
</View>
);
};
To modify your code, you should wrap the main navigator inside the LogContext.Provider and maintain the state there which will help you do the rest.
Feel free to ask any further clarification :)

Consistent left justify on phone application

I’m writing a small web application, hopefully it looks aesthetic on phone. On a mobile phone, the font showing the selected clinics are center justified, I want to know how to insert a line break into the code, so that the options appear consistently justified left in the mobile version, regardless of the name of the options.
Update: I still maintain a spaced-out view on the computer version, after the change.
The problem can be reproduced if 670150 is keyed as postal code, age 22, Singapore, eligible, CHAS Orange, and just select the first option of GP and polyclinics.
Phone view
Computer view
Can someone help me out here? Here's the relevant code.
ResultsTab.js
import React from "react";
import PropTypes from "prop-types";
import SwipeableViews from "react-swipeable-views";
import { makeStyles, useTheme } from "#material-ui/core/styles";
import AppBar from "#material-ui/core/AppBar";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
import Typography from "#material-ui/core/Typography";
// import MyMap from "./myMap";
import TestMap from "./TestMap";
import PcDialog from "./PcDialog";
import GpDialog from "./GpDialog";
// import { display } from "#material-ui/system";
import CompareDialog from "./CompareDialog";
import Grid from "#material-ui/core/Grid";
import Paper from "#material-ui/core/Paper";
function TabContainer({ children, dir }) {
return (
<Typography component="div" dir={dir} style={{ padding: 8 * 3 }}>
{children}
</Typography>
);
}
TabContainer.propTypes = {
children: PropTypes.node.isRequired,
dir: PropTypes.string.isRequired
};
const useStyles = makeStyles(theme => ({
root: {
backgroundColor: theme.palette.background.paper,
width: "100%"
}
}));
const ResultTabs = props => {
const classes = useStyles();
const theme = useTheme();
const [value, setValue] = React.useState(0);
const [state, setState] = React.useState({
sortByLoc: true
});
function handleChange(event, newValue) {
setValue(newValue);
}
function handleChangeIndex(index) {
setValue(index);
}
const [open, setOpen] = React.useState(false);
const [selectedGP, setSelectedGP] = React.useState({
properties: { HCI_NAME: "Please Choose a GP" },
distance: "x",
price: "x",
rating: "x"
});
const [selectedPC, setSelectedPC] = React.useState({
Name: "Please choose a Polyclinic",
distance: "x",
price: "x",
rating: "x"
});
const [GPName, setGPName] = React.useState("none");
const [PCName, setPCName] = React.useState("none");
function handleClickOpen() {
setOpen(true);
}
const handleGPClose = (clinic, name) => {
setOpen(false);
clinic.price = "$$";
clinic.rating = "4.3";
setSelectedGP(clinic);
setGPName(name);
};
const handlePCClose = (clinic, name) => {
setOpen(false);
clinic.price = "$";
clinic.rating = "4.0";
setSelectedPC(clinic);
setPCName(name);
};
return (
<div className={classes.root}>
<Grid style={{ flexGrow: 1 }} direction="row">
<Grid container justify="space-evenly">
<Grid item>Selected GP: {GPName}</Grid>
<Grid item>
<p style={{ fontSize: "1em" }}>Selected PolyClinic: {PCName}</p>
{/* {console.log(selectedGP)} */}
</Grid>
</Grid>
</Grid>
<Grid style={{ flexGrow: 1 }} direction="row">
<Grid container justify="center">
<CompareDialog
GP={selectedGP}
PC={selectedPC}
formData={props.formData}
/>
</Grid>
</Grid>
<hr />
<AppBar position="static" color="default">
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
>
<Tab label="GP" />
<Tab label="Polyclinic" />
<Tab label="Map View" />
</Tabs>
</AppBar>
<SwipeableViews
axis={theme.direction === "rtl" ? "x-reverse" : "x"}
index={value}
onChangeIndex={handleChangeIndex}
>
<TabContainer dir={theme.direction}>
{props.GP.map(clinic => {
return (
<div key={clinic.properties.id}>
<GpDialog
clinic={clinic}
selectedGP={selectedGP}
open={open}
onClose={handleGPClose}
/>
<hr />
</div>
);
})}
</TabContainer>
<TabContainer dir={theme.direction}>
{props.PC.map(clinic => {
return (
<div key={clinic.id}>
<PcDialog
clinic={clinic}
selectedPC={selectedGP}
open={open}
onClose={handlePCClose}
/>
<hr />
</div>
);
})}
</TabContainer>
<TabContainer dir={theme.direction}>
{props.currentLoc[0] !== 0 && (
<TestMap coord={props.currentLoc} GP={props.GP} PC={props.PC} />
)}
</TabContainer>
</SwipeableViews>
</div>
);
};
export default ResultTabs;
FilteredResults.js
import React from "react";
import GP from "./chas.json";
import * as turf from "#turf/turf";
import ResultTabs from "./ResultTabs.js";
import PC from "./polyclinics.json";
import Button from "#material-ui/core/Button";
import Switch from "#material-ui/core/Switch";
import Grid from "#material-ui/core/Grid";
const API_KEY = "";
// this component aims to display the filtered clinic after they fill in the form
//try not to abuse the API call, im using some kind of free credits from google for this
//api to be able to consistenly make the api call
//api in use here are: google geocode & turf
//everything works except for styling, but the content from the json file abit lacking,
// no opening hrs etc
class FilteredResult extends React.Component {
constructor(props) {
super(props);
this.state = {
formData: this.props.location.state, //this gets the info from react router from Form.js
userLng: 0,
userLat: 0,
sortByLoc: true
};
this.goBack = this.goBack.bind(this);
}
componentDidMount() {
fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${
this.state.formData.postalCode
}&region=sg&key=${API_KEY}`
)
.then(res => res.json())
.then(json => {
this.setState({
userLng: json.results[0].geometry.location.lng,
userLat: json.results[0].geometry.location.lat
});
});
}
goBack() {
this.props.history.goBack();
}
render(props) {
const { userLat, userLng, formData } = this.state;
const filteredGP = GP.features.filter(clinic => {
const from = turf.point([userLng, userLat]);
const to = turf.point([
clinic.geometry.coordinates[0],
clinic.geometry.coordinates[1]
]);
const options = { units: "kilometers" };
const dist = turf.distance(from, to, options);
clinic.distance = dist;
if (formData.hasSubsidy === "Yes") {
return (
dist <= 3
// && clinic.properties.CLINIC_PROGRAMME_CODE.includes(formData.subsidyType)
);
}
return dist <= 3;
});
const filteredPC = PC.clinics.filter(clinic => {
const from = turf.point([userLng, userLat]);
const to = turf.point([clinic.coord[0], clinic.coord[1]]);
const options = { units: "kilometers" };
const dist = turf.distance(from, to, options);
clinic.distance = dist;
return dist <= 100;
});
function sortDist(a, b) {
if (a.distance < b.distance) {
return -1;
} else {
return 1;
}
}
const handleSwitch = name => event => {
this.setState({ [name]: event.target.checked });
};
const sortedGP = filteredGP.sort(sortDist);
const sortedPC = filteredPC.sort(sortDist);
//note: dangerouslySetInnerHTML cos the json is in string, but its actually HTML
return (
<div>
<Grid container justify="center">
<h2>
Filtered clinics for{" "}
<span style={{ fongWeight: "bold", textDecoration: "underline" }}>
S{formData.postalCode}
</span> {" "}
{formData.subsidyType === ""
? ""
: `with ${formData.subsidyType} subsidy`}
</h2>
</Grid>
{/* <Switch
checked={this.state.sortByLoc}
onChange={handleSwitch('sortByLoc')}
value="sortByLoc"
inputProps={{ 'aria-label': 'secondary checkbox' }}
/> */}
<div>
<hr />
<ResultTabs
GP={sortedGP}
PC={sortedPC}
formData={formData}
currentLoc={[this.state.userLng, this.state.userLat]}
/>
<Button onClick={this.goBack}>Go Back</Button>
</div>
</div>
);
}
}
export default FilteredResult;
In the ResultTab.js inside the return give justify="left" instead of "space-evenly" for the Grid and I hope you will achieve what you wanted for phone application
<Grid container justify="left">
<Grid item>Selected GP: {GPName}</Grid>
<Grid item>
<p style={{ fontSize: "1em" }}>Selected PolyClinic: {PCName}</p>
{/* {console.log(selectedGP)} */}
</Grid>
</Grid>
</Grid>

Need to dynamically add items into a drawer menu in React Native

I need to have some items dynamically in my app's drawer after some categories get fetched from a json file (https://www.rallyssimo.it/wp-json/wp/v2/categories)
json example (I need that information)
[
{
"id": 44,
.
.
"name": "ALTRI RALLY",
.
.
},
Tis is the drawer:
const CustomDrawerComponent = (props) => (
<SafeAreaView style={{flex:1}}>
<View style={{height:80, backgroundColor: 'white', alignItems: 'center', justifyContent: 'center'}}>
<Image
source={{uri: 'https://www.rallyssimo.it/wp-content/uploads/2016/08/rallyssimo-logo.png'}}
style={{ height: 60, width: 180}}
/>
</View>
<ScrollView>
<DrawerItems {...props} />
</ScrollView>
</SafeAreaView>
)
const AppNavigator = createDrawerNavigator(
{
Home: DashboardStackNavigator,
},
{
contentComponent: CustomDrawerComponent
}
);
const AppContainer = createAppContainer(AppNavigator);
//Main class
export default class App extends React.Component {
render() {
return <AppContainer />;
}
}
How can I put the items (I'm going to get from the JSON) in the drawer?
As you have noticed, you need to create your own custom drawer to achieve this, which is done with contentComponent: CustomDrawerComponent.
Now you cannot use DrawerItems within CustomDrawerComponent since you want full control on the items listed. But you can recreate the items yourself using basic and elements.
Finally you need to fetch the API and store the data in your state in order to render the result as a list in the drawer.
Here is a basic example for :
import React, { Component } from 'react';
import { ScrollView, Text, View, Image } from 'react-native';
import { NavigationActions } from 'react-navigation';
class CustomDrawerComponent extends Component {
constructor(props) {
super(props);
this.state = { data: null };
}
async componentDidMount() {
fetch('https://www.rallyssimo.it/wp-json/wp/v2/categories')
.then(res => res.json())
.then(data => this.setState({ data }))
}
navigateToScreen(routeName, params) {
return () => { this.props.navigation.dispatch(NavigationActions.navigate({ routeName, params })) };
}
render() {
if (this.state.data === null) {
return <Text>...</Text>;
}
return (
<View style={{ flex: 1, paddingTop: 30 }}>
<View style={{height:80, backgroundColor: 'white', alignItems: 'center', justifyContent: 'center'}}>
<Image
source={{uri: 'https://www.rallyssimo.it/wp-content/uploads/2016/08/rallyssimo-logo.png'}}
style={{ height: 60, width: 180}}
/>
</View>
<ScrollView>
<View>
{this.state.data.map(x => (
<Text
key={x.id}
style={{ fontSize: 16, lineHeight: 30, textAlign: 'center' }}
onPress={this.navigateToScreen('page2')}
>
{x.name}
</Text>
))}
</View>
</ScrollView>
</View>
);
}
}
export default CustomDrawerComponent;
And here is a working snack.

Array JSON with ListView in React-Native

I have an issue, I'm trying to make a little database offline inside a JSON, like this:
[
{
title: "Carros",
carros: [
{
nome: "Ferrari"
}
]
},
{
title: "Motos",
carros: [
{
nome: "Suzuki"
}
]
}
];
From now, my HomeScreen lists the categories as "Carros" and "Motos", but when I want to enter in subtopic like "carros", but I can't.
Currently using a ListView
{ list.map((item, i) => (
<View>
<TouchableOpacity
onPress={() =>
this.props.navigation.navigate("Listagem", {
itemName: item.title
})
}
>
<ListItem key={item.title} title={item.title} />
</TouchableOpacity>
</View>
))
}
How to get child items?
Carros -> carros
Motos -> carros ???
Motos -> motos
If you only have "carros", there are no "motos". Hope you get my point.
In order to get the carros Object inside the Listagem screen you need to pass it as an prop.
<TouchableOpacity
onPress={() =>
this.props.navigation.navigate("Listagem", {
itemName: item.title,
childItems: item.carros
})
}
>
In you Listagem screen you will get these properties.
constructor() {
super();
const { navigation } = this.props;
const itemName = navigation.getParam('itemName', 'defaultName');
const childItems = navigation.getParam('childItems', []);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
this.state = {
dataSource: ds.cloneWithRows(childItems),
};
}
render(){
return(
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text>{rowData.nome}</Text>}
/>
)
}
You can do this in your home screen:
import React, { Component } from 'react';
import {FlatList, ScrollView} from 'react-native';
import { List, ListItem } from 'react-native-elements'; //this is not necessary. You may use it for aesthetic purposes only.
const cars = [
{
title: "Carros",
carros: [
{
nome: "Ferrari"
}
]
},
{
title: "Motos",
carros: [
{
nome: "Suzuki"
}
]
}
];
export default class Home extends Component {
renderCars() {
return (
<List
containerStyle={{ borderTopWidth: 0, borderBottomWidth: 0 }}
>
<FlatList
data={cars}
keyExtractor={item => item.title}
renderItem={({ item }) => (
<ListItem
titleStyle={yourstyles.title}
hideChevron
title={item.title}
onPress={() => this.props.navigation.navigate(
'ChildPage',
{
cars: item //pass the entire item as a prop. This way you will have all its subtopics in your child page
}
)}
/>
)}
/>
</List>
)
}
render() {
return (
<ScrollView>
{this.renderCars()}
</ScrollView>
)
}
}
In your child page where you want to list the subtopics of 'carros' for example. Remember you passed the entire item as a prop, so that prop will be available for you in the child page.
So now you can do this in your child page:
//ChildPage
import React, { Component } from 'react';
import {FlatList, ScrollView, View, Text} from 'react-native';
import { List, ListItem } from 'react-native-elements'; //this is not necessary. You may use it for aesthetic purposes only.
export default class ChildPage extends Component {
renderSubtopics() {
const { cars } = this.props.navigation.state.params; // remember the entire item you passed down from Home, we will use it here
return (
<List
containerStyle={{ borderTopWidth: 0, borderBottomWidth: 0 }}
>
<FlatList
data={cars.carros}
keyExtractor={item => item.nome}
renderItem={({ item }) => (
<ListItem
titleStyle={yourstyles.title}
hideChevron
title={item.nome}
onPress={() => //do whatever )}
/>
)}
/>
</List>
)
}
render() {
return (
<ScrollView>
<View>
<Text>
{this.props.navigation.state.params.cars.title}
<Text>
</View>
{this.renderSubtopics()}
</ScrollView>
)
}
}