Refactoring large View component in React - html

I have a rather large view component to render a form where the user can add comments, upload images, select between multiple other products and submit it. I keep my state and functions in a separate component and am quite happy with that setup.
The view has grown quite substantially and I am not entirely happy with the structure of the app.
Particularly it annoys me that
I have to pass the same props and classes variables to each component I use
I have a mix of logic in my main view component CreateEntryForm and each of the smaller components
Any tips on how to better structure this code is much appreciated
import React from 'react';
import TextField from 'material-ui/TextField';
import Button from 'material-ui/Button';
import { MenuItem } from 'material-ui/Menu';
import { withStyles } from 'material-ui/styles';
import Chip from 'material-ui/Chip';
import Typography from 'material-ui/Typography';
import { CircularProgress } from 'material-ui/Progress';
import EntryImageView from "./entryImageView";
import {MarkedImage} from "./markedImage";
const styles = theme => ({
button: {
margin: theme.spacing.unit,
},
container: {
display: 'flex',
flexWrap: 'wrap',
},
menu: {
width: 200,
},
chipContainer: {
marginTop: theme.spacing.unit * 2,
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: theme.spacing.unit / 2,
},
wrapper: {
marginTop: theme.spacing.unit*2
},
});
const ProductFormGroup = (props) => (
<div>
<TextField
id="selectedProduct"
required
select
label="Select a product"
error={props.selectProductError !== ''}
margin="normal"
fullWidth
value={(props.currentEntry.currentProduct === undefined || props.currentEntry.currentProduct === null) ? "" : props.currentEntry.currentProduct}
onChange={props.handleInputChange('currentProduct')}
SelectProps={{
MenuProps: {
className: props.menu,
},
name: "currentProduct"
}}
helperText={props.selectProductError || ""}
>
{props.job.selectedProducts.map((product, index) => (
<MenuItem key={index} value={product}>
{product.name}
</MenuItem>
))}
</TextField>
<TextField
margin="dense"
id="productQuantity"
label="Product Quantity"
fullWidth
name="productQuantity"
onChange={props.handleInputChange('productQuantity')}
value={props.productQuantity} />
<Button color="primary"
onClick={() => props.addSelectedChip()}
disabled={(props.productQuantity === '' || props.currentEntry.currentProduct === null)}>
add product
</Button>
</div>
);
const SelectedProductsGroup = (props) => (
<div>
<TextField
id="currentUpload"
select
label="Select an image to mark"
margin="normal"
fullWidth
value={props.currentUpload || ''}
onChange={props.handleInputChange('currentUpload')}
SelectProps={{
MenuProps: {
className: props.menu,
},
name: "currentUpload"
}}
>
{props.job.selectedUploads.map((file, index) => (
<MenuItem key={index} value={file}>
{file.name}
</MenuItem>
))}
</TextField>
{props.currentUpload &&
<Button color="primary" onClick={() => props.handleAttachmentDialogOpen(props.job.selectedUploads)}>
Edit marker on image
</Button>}
{props.selectedMarkedImage &&
<MarkedImage markerPosition={props.selectedMarkedImage.position}
attachment={props.selectedMarkedImage.attachment}
imageLoaded={props.markedImageLoaded}
handleImageLoaded={props.handleMarkedImageLoaded}
/>}
</div>
);
const SelectedProductsChipContainer = (props) => (
<div className={props.classes.wrapper}>
<Typography type="subheading" gutterBottom>
Selected Products
</Typography>
<div className={props.classes.chipContainer}>
{props.selectedProducts.map((product, index) => {
return (
<Chip
label={`${product.name} (${product.productQuantity})`}
key={index}
className={props.classes.chip}
onRequestDelete={() => props.handleRequestDeleteChip(product, "product")}
/>
)
})}
</div>
</div>
);
const SelectedImagesView = (props) => (
<div className={props.classes.wrapper}>
<Typography type="subheading" gutterBottom>
Selected images
</Typography>
<input type="file" id="myFile" onChange={props.handleFileUpload} />
{props.uploadLoading
? <CircularProgress/>
: null}
{props.selectedUploads.length > 0 && <EntryImageView selectedUploads={props.selectedUploads}
handleRequestDeleteChip={props.handleRequestDeleteChip} />}
</div>
);
const LocationDescriptionTextField = (props) => (
<TextField
id="locationDescription"
label="Location Description"
multiline
rows="4"
value={props.locationDescription}
onChange={props.handleInputChange('locationDescription')}
margin="normal"
fullWidth
/>
);
const CommentsTextField = (props) => (
<TextField
id="comments"
label="Comments"
multiline
rows="4"
value={props.comments}
onChange={props.handleInputChange('comments')}
margin="normal"
fullWidth
/>
);
export const CreateEntryForm = (props) => {
const { classes } = props;
return (
<div>
{props.job && <SelectedProductsGroup {...props} classes={classes}/>}
{props.selectedProducts.length > 0 && <SelectedProductsChipContainer {...props} classes={classes}/>}
{props.job && <ProductFormGroup {...props}/>}
<SelectedImagesView {...props} classes={classes} />
<LocationDescriptionTextField {...props} />
<CommentsTextField {...props} />
<Button color="primary" onClick={props.handleSubmit}>Create entry</Button>
</div>
)
};
export default withStyles(styles)(CreateEntryForm);

Related

Three Rows Table using Material-UI and React

I need to achieve something like this in the picture.
I need to create a table with three rows, where the third row is below the first and second rows, and the width of the third row is the combined width of the first and second rows.
CODESANDBOX: CLICK HERE FOR CODESANDBOX
CODE
const CustomTable = () => {
const handleSubmit = () => {};
return (
<TableContainer component={Paper}>
<Formik
initialValues={[
{
id: 1,
attribute: "",
thirdRow: ""
}
]}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form>
<FieldArray
name="rows"
render={(arrayHelpers) => (
<React.Fragment>
<Box>
<Button
variant="contained"
type="submit"
startIcon={<AddIcon />}
onClick={() =>
arrayHelpers.unshift({
id: Date.now(),
attribute: "",
ruleId: "",
thirdRow: ""
})
}
>
Add New
</Button>
</Box>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Attribute</TableCell>
<TableCell>
<span />
Rule ID
</TableCell>
<TableCell colSpan={2}>Third Row</TableCell>
</TableRow>
</TableHead>
<TableBody>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</TableBody>
</Table>
</React.Fragment>
)}
/>
</Form>
)}
</Formik>
</TableContainer>
);
};
export default CustomTable;
You could tweak each "row" to really render 2 rows where the second row's column spans 2 column widths.
demo.js
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow
sx={{
"th": { border: 0 }
}}
>
<TableCell>Attribute</TableCell>
<TableCell>Rule ID</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2}>Third Row</TableCell>
</TableRow>
</TableHead>
<TableBody>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</TableBody>
</Table>
CustomTableRow
const CustomTableRow = ({ row, index }) => {
return (
<>
<TableRow
sx={{
"th, td": { border: 0 }
}}
>
<TableCell component="th" scope="row">
<FastField
name={`rows.${index}.attribute`}
component={TextField}
fullWidth
/>
</TableCell>
<TableCell>
<FastField
name={`rows.${index}.ruleId`}
component={TextField}
fullWidth
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell colSpan={2}>
<FastField
name={`rows.${index}.thirdRow`}
component={TextField}
fullWidth
/>
</TableCell>
</TableRow>
</>
);
};
I prefer to use grid layout. You won't then use Table elements but will then want to give the role attribute for accessibility.
The nice thing about using grid layout is that you get a lot of flexibility so you can then say check for [theme.breakpoints.down("md")] and adjust your gridTemplateAreas accordingly.
So, you would do something like the following to get all the 3 rows stacked one over the other.
[theme.breakpoints.down("md")]: {
gridTemplateColumns: "1fr",
gridTemplateAreas: `"upper1"
"upper2"
"bottom"`,
gap: "0px",
}
The other benefit of using gridArea is that the display is visual so it is easy to see as you are designing your layout
I forked your codesanbox and the link and it is at https://codesandbox.io/s/dry-tdd-vv6z1s?file=/demo.js
The complete code is given below.
import React from "react";
import Table from "#mui/material/Table";
import TableBody from "#mui/material/TableBody";
import TableCell from "#mui/material/TableCell";
import TableContainer from "#mui/material/TableContainer";
import TableHead from "#mui/material/TableHead";
import TableRow from "#mui/material/TableRow";
import Paper from "#mui/material/Paper";
import { Box, Button } from "#mui/material";
import AddIcon from "#mui/icons-material/Add";
import { FieldArray, Form, Formik } from "formik";
import CustomTableRow from "./CustomTableRow";
import { styled } from "#mui/material/styles";
const Wrapper = styled(Box)(({ theme }) => ({
display: "grid",
gridTemplateColumns: "1fr 1fr",
gridTemplateAreas: `"upper1 upper2 "
"bottom bottom"`,
gap: "0px"
}));
const Upper1 = styled(Box)(({ theme }) => ({
gridArea: "upper1"
}));
const Upper2 = styled(Box)(({ theme }) => ({
gridArea: "upper2"
}));
const Bottom = styled(Box)(({ theme }) => ({
gridArea: "bottom"
}));
const CustomTable = () => {
const handleSubmit = () => {};
return (
<TableContainer component={Paper}>
<Formik
initialValues={[
{
id: 1,
attribute: "",
ruleId: "",
thirdRow: ""
}
]}
onSubmit={handleSubmit}
>
{({ values }) => (
<Form>
<FieldArray
name="rows"
render={(arrayHelpers) => (
<React.Fragment>
<Box>
<Button
variant="contained"
type="submit"
startIcon={<AddIcon />}
onClick={() =>
arrayHelpers.unshift({
id: Date.now(),
attribute: "",
ruleId: "",
thirdRow: ""
})
}
>
Add New
</Button>
</Box>
<Box aria-label="simple table">
<Wrapper>
<Upper1 style={{ border: "2px blue solid" }}>
Attribute
</Upper1>
<Upper2 style={{ border: "2px blue solid" }}>
Rule ID
</Upper2>
<Bottom style={{ border: "2px blue solid" }}>
Third Row
</Bottom>
</Wrapper>
<Box>
{values.rows?.map((row, index) => (
<CustomTableRow
key={row.id}
row={row}
index={index}
arrayHelpers={arrayHelpers}
/>
))}
</Box>
</Box>
</React.Fragment>
)}
/>
</Form>
)}
</Formik>
</TableContainer>
);
};
export default CustomTable;

How to set State as an empty array for input field in react js

I am trying to create upload form with multiple input in my input I apply multiple attribute it is working fine with loop but I have problem in storing files in state as an array I am allowing input as conditional rendering but my state is empty array but not null so input is not available code of form is as below
Upload form
import React, { useEffect, useState } from "react";
import { IoCloudUpload } from "react-icons/io5";
import { doc, setDoc } from "firebase/firestore";
import { getFirestore } from "firebase/firestore";
import {
Flex,
useColorMode,
useColorModeValue,
Input,
Button,
FormLabel,
Text,
} from "#chakra-ui/react";
import ProgressBar from "../ProgressBar/ProgressBar";
import {
getStorage,
ref,
uploadBytesResumable,
getDownloadURL,
} from "firebase/storage";
import { firebaseApp } from "../../../firebase.config";
export default function UploadForm() {
const { colorMode } = useColorMode();
const bg = useColorModeValue("gray.50", "gray.900");
const textColor = useColorModeValue("gray.900", "gray.50");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [price, setPrice] = useState(0);
const [place, setPlace] = useState("");
const [fueltype, setFuelType] = useState("");
const [progress, setProgress] = useState(1);
const [loading, setLoading] = useState(false);
const [imageAsset, setImageAsset] = useState([]);
const [urls, setUrls] = useState([]);
const storage = getStorage(firebaseApp); // Firebase Storage
const firebaseDb = getFirestore(firebaseApp); // Firebase FireStore
function UploadImage(e) {
e.preventDefault();
setLoading(true);
for (let i = 0; i < e.target.files.length; i++) {
const Images = e.target.files[i];
Images["id"] = Math.random();
setImageAsset((prevState) => [...prevState, Images]);
}
const promises = []
imageAsset.map((image) => {
const storageRef = ref(storage, `Images/${Date.now()}-${image.name}}`);
const uploadTask = uploadBytesResumable(storageRef, image);
// promises.push(uploadTask);
uploadTask.on(
"state_changed",
(snapShot) => {
const uploadProgress =
(snapShot.bytesTransferred / snapShot.totalBytes) * 100;
setProgress(uploadProgress);
},
(error) => {
console.log(error);
},
async () => {
await getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
setUrls((prevState)=> [...prevState, downloadURL]);
setLoading(false);
});
},
// Promise.all(promises)
// .then(()=> alert('done'))
// .catch((err)=> console.log(err))
);
});
}
async function uploadData() {
try {
setLoading(true);
if (!imageAsset && !title && !description) {
console.log("no data");
} else {
const data = {
id: `${Date.now()}`,
Title: title,
Description: description,
imageURL: `${urls}`,
Price: price,
Place: place,
Fuel: fueltype,
};
await setDoc(doc(firebaseDb, "Images", `${Date.now()}`), data);
setLoading(false);
}
} catch (error) {
console.log(error);
}
}
console.log(imageAsset);
console.log(progress)
useEffect(() => {}, [title, description, imageAsset]);
return (
<Flex
justifyContent={"center"}
alignItems="center"
width={"full"}
minHeight="100vh"
padding={10}
>
<Flex
width={"80%"}
height="full"
border={"1px"}
borderColor="gray.300"
borderRadius={"md"}
p="4"
flexDirection={"column"}
alignItems="center"
justifyContent={"center"}
gap={2}
>
<Input
variant={"flushed"}
placeholder="Title"
focusBorderColor="gary.400"
isRequired
errorBorderColor="red"
type={"text"}
_placeholder={{ color: "gray.500" }}
fontSize={20}
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<Flex
border={"1px"}
borderColor="gray.500"
height={"400px"}
borderStyle="dashed"
width={"full"}
borderRadius="md"
overflow={"hidden"}
position="relative"
>
{!imageAsset ? ( // Problem is here it is not null
<FormLabel width={"full"}>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
cursor="pointer"
>
{loading ? (
<ProgressBar msg={"Uploading Image"} progress={progress} />
) : (
<>
<IoCloudUpload
fontSize={"8rem"}
color={`${colorMode == "dark" ? "#f1f1f1" : "#111"}`}
/>
<Text mt={5} fontSize={20} color={textColor}>
Click To Upload
</Text>
</>
)}
</Flex>
</Flex>
{!loading && (
<input
type={"file"}
name="Images"
multiple
style={{ width: 0, height: 0 }}
accept="image/jpg, image/png, image/jpeg"
onChange={UploadImage}
/>
)}
</FormLabel>
) : (
<Flex
justifyContent={"center"}
alignItems="center"
width={"full"}
height="full"
bg={"black"}
position={"relative"}
>
<img src={imageAsset} style={{ width: "100%", height: "100%" }} />
</Flex>
)}
</Flex>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
>
<Input
variant={"flushed"}
placeholder="Description"
focusBorderColor="gary.400"
isRequired
errorBorderColor="red"
type={"text"}
_placeholder={{ color: "gray.500" }}
fontSize={20}
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</Flex>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
>
<Input
variant={"flushed"}
placeholder="Price"
focusBorderColor="gary.400"
isRequired
errorBorderColor="red"
type={"text"}
_placeholder={{ color: "gray.500" }}
fontSize={20}
value={price}
onChange={(e) => setPrice(e.target.value)}
/>
</Flex>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
>
<Input
variant={"flushed"}
placeholder="Place"
focusBorderColor="gary.400"
isRequired
errorBorderColor="red"
type={"text"}
_placeholder={{ color: "gray.500" }}
fontSize={20}
value={place}
onChange={(e) => setPlace(e.target.value)}
/>
</Flex>
<Flex
direction={"column"}
alignItems="center"
justifyContent={"center"}
height="full"
width={"full"}
>
<Input
variant={"flushed"}
placeholder="Fuel Type"
focusBorderColor="gary.400"
isRequired
errorBorderColor="red"
type={"text"}
_placeholder={{ color: "gray.500" }}
fontSize={20}
value={fueltype}
onChange={(e) => setFuelType(e.target.value)}
/>
</Flex>
<Button
marginTop={"1rem"}
isLoading={loading}
loadingText="Adding File"
colorScheme={"teal"}
variant={`${loading ? "outline" : "solid"}`}
width="full"
_hover={{ shadow: "lg" }}
fontSize={20}
onClick={uploadData}
>
Upload File
</Button>
</Flex>
</Flex>
);
}
I added a comment where I am getting non null array I don't know how to solve this problem.....

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

Render input fields dynamically inside a list

I have set of components where it would consist of input fields along with text rows.
As given in the image the users should be able to add categories and description. After adding them they will be rendered as a list of components. like this
Inside a category there will be tags as given in the above image and to add them i have to add a input component. This input component should be available only when the user clicks on the Add tag button below each category row. When a user clicks on it,it should enable the input(should render a input component inside the selected category row) and should be able to type the tag name on it and save it. I need to make this input field enable only when i click on the add tag button. and it should enable only in the selected category row. This is the code that i have tried.
import React, { Component, Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
class App extends Component {
state = {
category: "",
description: "",
categories: []
};
onChange = (e, { name, value }) => {
this.setState({ [name]: value });
};
addCategory = () => {
let { category, description } = this.state;
this.setState(prevState => ({
categories: [
...prevState.categories,
{
id: Math.random(),
title: category,
description: description,
tags: []
}
]
}));
};
addTag = id => {
let { tag, categories } = this.state;
let category = categories.find(cat => cat.id === id);
let index = categories.findIndex(cat => cat.id === id);
category.tags = [...category.tags, { name: tag }];
this.setState({
categories: [
...categories.slice(0, index),
category,
...categories.slice(++index)
]
});
};
onKeyDown = e => {
if (e.key === "Enter" && !e.shiftKey) {
console.log(e.target.value);
}
};
tags = tags => {
if (tags && tags.length > 0) {
return tags.map((tag, i) => {
return <Header key={i}>{tag.name}</Header>;
});
}
};
enableTagIn = id => {};
categories = () => {
let { categories } = this.state;
return categories.map(cat => {
return (
<Fragment key={cat.id}>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
onKeyDown={e => {
this.onKeyDown(e);
}}
onChange={this.onChange}
/>
<Button
onClick={e => {
this.addTag(cat.id);
}}
>
Add
</Button>
{this.tags(cat.tags)}
</Fragment>
);
});
};
render() {
return (
<Fragment>
{this.categories()}
<div>
<Input name="category" onChange={this.onChange} />
<Input name="description" onChange={this.onChange} />
<Button onClick={this.addCategory}>Save</Button>
</div>
</Fragment>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
This is the codesandbox url.
Any idea on how to achieve this?.
I changed your code by using function components and react hooks and i created category component which has it own state like this:
import React, { Fragment } from "react";
import { Button, Header, Input } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
import ReactDOM from "react-dom";
const App = () => {
const [Category, setCategory] = React.useState({
title: "",
description: ""
});
const [Categories, setCategories] = React.useState([]);
return (
<div>
{console.log(Categories)}
<Input
value={Category.title}
onChange={e => setCategory({ ...Category, title: e.target.value })}
/>
<Input
value={Category.description}
onChange={e =>
setCategory({ ...Category, description: e.target.value })
}
/>
<Button onClick={() => setCategories([...Categories, Category])}>
Save
</Button>
<div>
{Categories.length > 0
? Categories.map(cat => <CategoryItem cat={cat} />)
: null}
</div>
</div>
);
};
const CategoryItem = ({ cat }) => {
const [value, setvalue] = React.useState("");
const [tag, addtag] = React.useState([]);
const [clicked, setclicked] = React.useState(false);
const add = () => {
setclicked(false);
addtag([...tag, value]);
};
return (
<Fragment>
<Header>
<p>
{cat.title}
<br />
{cat.description}
</p>
</Header>
<Input
name="tag"
value={value}
style={{ display: clicked ? "initial" : "none" }}
onChange={e => setvalue(e.target.value)}
/>
<Button onClick={() => (clicked ? add() : setclicked(true))}>Add</Button>
<div>{tag.length > 0 ? tag.map(tagname => <p>{tagname}</p>) : null}</div>
</Fragment>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
and here a sandbox

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

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.