For the below line of code, how can I dispatch notificationsStateUpdate without onClick? I want this action to be dispatched if notificationClicked is true, so I currently have a ternary expression set up.
However, I can't seem to get the syntax to work. Is it possible to dispatch in this scenario?
{notificationClicked ?
<NotificationList
notifications={newNotifications} />
dispatch(notificationsStateUpdate({newNotifications}))
: null}
Full code for context
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch, connect } from 'react-redux';
import _ from 'lodash';
import {makeStyles, useTheme} from '#material-ui/core/styles';
import usePrevious from '../hooks/usePrevious';
import NotificationList from './NotificationList';
import { notificationsStateUpdate } from '../actions';
export default function Notifications(props) {
const [newNotifications, setNewNotifications] = useState([]);
const users = useSelector(state => state.users);
const notificationClicked = useSelector(state => state.notificationClicked)
const prevUsers = usePrevious(users);
const dispatch = useDispatch();
console.log('inside', users);
const isEqual = _.isEqual(prevUsers, users);
const timestamp = !isEqual ? new Date().getTime() : new Date("1991-09-24").getTime();
useEffect(() => {
const notifications = [];
console.log('users', users);
users.forEach((user) => {
if (user.uid === props.uid && user.posts) {
user.posts.forEach((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
if (likes){
let filtererdLikes = likes.filter(post => {
return post.like_notification === false
})
notifications.push(filtererdLikes)
}
if (comments){
let letfilteredComments = comments.filter(post => {
return post.comment_notification === false
})
notifications.push(letfilteredComments)
}
})
}
});
const notificationsDataClean = notifications.flat(Infinity)
setNewNotifications(notificationsDataClean);
}, [timestamp]);
const useStyles = makeStyles((theme) => ({
body: {
margin: '25',
background: '#3f51b5'
},
iconButton: {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 50,
height: 50,
color: '#333333',
background: '#dddddd',
border: 'none',
outline: 'none',
borderRadius: '50%',
'&:hover': {
cursor: 'pointer'
},
'&:active': {
background: '#cccccc'
}
},
iconButton__badge: {
position: 'absolute',
top: -10,
right: -10,
width: 25,
height: 25,
background: 'red',
color: '#ffffff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '50%'
}
}
));
const classes = useStyles();
const theme = useTheme();
return (
<div>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Icon Button Notification Badge</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head>
<body className={classes.body}>
<button type="button" className={classes.iconButton}>
<span class="material-icons">notifications</span>
<span className={classes.iconButton__badge}>{newNotifications.length}</span>
</button>
</body>
{notificationClicked ? <NotificationList notifications={newNotifications} /> dispatch(notificationsStateUpdate({newNotifications})) : null}
</div>
);
}
If I am understanding correctly, this should work
<NotificationList
notificationClicked, // pass in as prop instead of a ternary
notifications={newNotifications} />
then call your dispatch in useEffect in NotificationList
<NotificationList>
/////
useEffect =(() => {
//Whatever else
if (notificationClicked) {
dispatch(notificationsStateUpdate({newNotifications}))
}
},[notificationClicked])
Related
I'm currently using react-moveable and react-selecto packages to build a moveable component. But it seems to be a problem with these two main packages and Nextjs.
I'm using React v18.1.0 and Next v12.1.6
import React, { useRef, useState } from "react";
import Moveable from "react-moveable";
import Selecto from "react-selecto";
export default function MoveableProvider() {
const [targets, setTargets] = useState([]);
const [frameMap] = useState(() => new Map());
const selectoRef = useRef(null);
const moveableRef = useRef(null);
return (
<div className="container">
<Moveable
ref={moveableRef}
draggable={true}
target={targets}
onClickGroup={(e) => {
selectoRef.current.clickTarget(e.inputEvent, e.inputTarget);
}}
onDrag={(e) => {
const target = e.target;
const frame = frameMap.get(target);
frame.translate = e.beforeTranslate;
target.style.transform = `translate(${frame.translate[0]}px, ${frame.translate[1]}px)`;
}}
onDragStart={(e) => {
const target = e.target;
if (!frameMap.has(target)) {
frameMap.set(target, {
translate: [0, 0],
});
}
const frame = frameMap.get(target);
e.set(frame.translate);
}}
onDragGroupStart={(e) => {
e.events.forEach((ev) => {
const target = ev.target;
if (!frameMap.has(target)) {
frameMap.set(target, {
translate: [0, 0],
});
}
const frame = frameMap.get(target);
ev.set(frame.translate);
});
}}
onDragGroup={(e) => {
e.events.forEach((ev) => {
const target = ev.target;
const frame = frameMap.get(target);
frame.translate = ev.beforeTranslate;
target.style.transform = `translate(${frame.translate[0]}px, ${frame.translate[1]}px)`;
});
}}
/>
<Selecto
ref={selectoRef}
dragContainer={".elements"}
selectableTargets={[".selecto-area .cube"]}
hitRate={0}
selectByClick={true}
selectFromInside={false}
toggleContinueSelect={["shift"]}
ratio={0}
onDragStart={(e) => {
const moveable = moveableRef.current;
const target = e.inputEvent.target;
k;
if (
moveable.isMoveableElement(target) ||
targets.some((t) => t === target || t.contains(target))
) {
e.stop();
}
}}
onSelectEnd={(e) => {
const moveable = moveableRef.current;
setTargets(e.selected);
if (e.isDragStart) {
e.inputEvent.preventDefault();
setTimeout(() => {
moveable.dragStart(e.inputEvent);
});
}
}}
></Selecto>
<div className="elements selecto-area" style={{ display: "flex" }}>
<div
className="cube target"
style={{
width: 250,
height: 200,
background: "green",
}}
></div>
<div
className="cube target"
style={{
width: 250,
height: 200,
background: "yellow",
}}
></div>
<div
className="cube target"
style={{
width: 250,
height: 200,
background: "aqua",
}}
></div>
<div
className="cube target"
style={{
width: 250,
height: 200,
background: "red",
}}
></div>
</div>
<div className="empty elements"></div>
</div>
);
}
In this case onDragGroup doesn't work for no reason in Nextjs but single drag (onDrag) works fine.
Is there any way that SSR would be causing the problem?
I m using textdecoration none in my link but when run the app it's not working can anyone state me solution for it?
Tysm for help in advance!
Navbar.js file
import React,{useState} from "react";
import { makeStyles } from "#material-ui/core/styles";
import {AppBar, Toolbar, Tab, Tabs, IconButton, Menu, MenuItem, useMediaQuery, useTheme, Button} from "#material-ui/core";
import MenuIcon from "#material-ui/icons/Menu";
import { Link } from "react-router-dom";
import {navlinks} from './navbarlinks';
import {FaFacebook} from 'react-icons/fa';
import {FaInstagram} from 'react-icons/fa';
import {FaLinkedinIn} from 'react-icons/fa';
import {FaTwitter} from 'react-icons/fa';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1
},
menuButton: {
marginRight: theme.spacing(2)
},
tab : {
color : '#000000',
'textDecoration': 'none'
},
appBarTransparent: {
backgroundColor: 'rgba(255,255,255,0.1);'
},
appBarSolid:{
}
}));
const Navbar = () => {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState(null);
const [anchorE2, setAnchorE2] = React.useState(null);
const open = Boolean(anchorEl);
const [value,setValue] = useState(0);
const handleMenu = (event) => {
setAnchorEl(event.currentTarget);
};
const handleOpenMenu = e => {
setAnchorE2(e.currentTarget);
}
const handleMenuClose = e => {
setAnchorE2(null);
}
const handleClose = () => {
setAnchorEl(null);
};
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
const handleClickTab = (e, newValue) => {
setValue(newValue);
}
const theme = useTheme();
const isMatch = useMediaQuery(theme.breakpoints.down('sm') );
return (
<div>
<AppBar position="sticky" className={classes.appBarTransparent}>
<Toolbar>
<Link to='/'><img src="/images/image2vector.svg" alt='logo' style={{'border-radius' : '33%', 'maxHeight' : 'auto', 'maxWidth':'112px'}}/></Link>
{ !isMatch ?
<div className={classes.root}>
<Tabs onChange={handleClickTab} indicatorColor='secondary' value={value} aria-label="icon tabs example">
{
navlinks.map( link => {
return <Link to={link.to}><Tab className={classes.tab} style={{'textDecoration': 'none'}}label={link.title} {...a11yProps(link.id)} /></Link>
})
}
</Tabs>
</div>
:
<div className={classes.root}>
<IconButton
aria-label="account of current user"
aria-controls="menu-appbar"
aria-haspopup="true"
onClick={handleMenu}
color="inherit"
>
<MenuIcon />
</IconButton>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right"
}}
open={open}
onClose={handleClose}
disableRipple
>
{ navlinks.map(link => {
return <MenuItem onClick={handleClose}><Link to={link.to}>{link.title}</Link></MenuItem>
})
}
</Menu>
</div>
}
<div>
<Button
color="secondary"
onClick={handleOpenMenu}
aria-controls='menu'
>
Contact Us
</Button>
</div>
<Menu
id='menu'
anchorE2={anchorE2}
open={Boolean(anchorE2)}
onClose={handleMenuClose}
anchorOrigin={{
vertical: "bottom",
horizontal: "right"
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "center"
}}
> <MenuItem>+91 9426231824</MenuItem>
<MenuItem><FaFacebook /></MenuItem>
<MenuItem><FaInstagram /></MenuItem>
<MenuItem><FaLinkedinIn /></MenuItem>
<MenuItem><FaTwitter /></MenuItem>
</Menu>
</Toolbar>
</AppBar>
</div>
);
};
export default Navbar;
navbarlink.js
export const navlinks = [
{
title: 'Home',
to : '/',
id:0
},
{
title: 'About US',
to : '/aboutus',
id:1
},
{
title: 'Projects',
to : '/projects',
id:2
},
{
title: 'Services',
to : '/services',
id:3
},
{
title: 'Get A Quote',
to : '/contactus',
id:4
}
]
I m using textdecoration none in my link but when run the app it's not working can anyone state me solution for it?
Tysm for help in advance!
I solved it with installing styled-componets and then putting following code
const StyledLink = styled(Link) `
text-decoration: none;
&:focus, &:hover, &:visited, &:link, &:active {
text-decoration: none;
`;
Remove the quotes from 'textDecoration'
So replace this line:
return <Link to={link.to}><Tab className={classes.tab} style={{'textDecoration': 'none'}}label={link.title} {...a11yProps(link.id)} /></Link>
to this:
return <Link to={link.to}><Tab className={classes.tab} style={{textDecoration: 'none'}}label={link.title} {...a11yProps(link.id)} /></Link>
I have Dialog component where I have a button(bottom right corner) which shows another div with an item list. The problem I'm facing is that the list is being rendered inside the Dialog component and it's being cropped. I set the position: absolute, z-index and set the position: relative to the parent but it does not work. Here is how it looks like. Any helpful tips I would appreciate it.
1) Before I click the button to show the list
2) After I click the button. Highlighted css properties for the list element
And the code for Dialog component :
import React, { useState } from "react";
import { makeStyles } from "#material-ui/core";
import Button from "#material-ui/core/Button";
import Dialog from "#material-ui/core/Dialog";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
import DialogTitle from "#material-ui/core/DialogTitle";
import IconButton from "#material-ui/core/IconButton";
import CloseIcon from "#material-ui/icons/Close";
import Typography from "#material-ui/core/Typography";
import OutlinedInput from "#material-ui/core/OutlinedInput";
import { ProjectTreeWindow } from "../index";
const useStyles = makeStyles(theme => ({
addTaskButton: {
marginTop: 10
},
dialog: {
width: "40%",
maxHeight: 435
},
closeButton: {
position: "absolute",
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
},
controlsWrapper: {
display: "flex",
alignItems: "center",
justifyContent: "space-between"
}
}));
const AddQuickTaskDialog = props => {
const classes = useStyles(props);
const { open, close } = props;
const [quickTaskDescription, setQuickTaskDescription] = useState("");
const [textInputRef, setTextInputRef] = useState(null);
const handleChangeQuickTaskDescription = event => {
setQuickTaskDescription(event.target.value);
};
const handleAddQuickTaskSubmit = () => {
alert("Quick task submitted");
close(textInputRef);
};
return (
<Dialog
data-testid="add-task-quick"
classes={{
paper: classes.dialog
}}
maxWidth="lg"
open={open}
keepMounted
onClose={() => {
close(textInputRef);
}}
aria-labelledby="quick-task-dialog"
aria-describedby="quick-task-dialog-description"
>
<DialogTitle id="quick-task-dialog-title">
<Typography variant="h6">Quick Add Task</Typography>
{close ? (
<IconButton
aria-label="close"
className={classes.closeButton}
onClick={() => {
close(textInputRef);
}}
>
<CloseIcon />
</IconButton>
) : null}
</DialogTitle>
<DialogContent>
<div className={classes.wrapper}>
<OutlinedInput
onChange={handleChangeQuickTaskDescription}
inputRef={input => {
setTextInputRef(input);
return input && input.focus();
}}
fullWidth
// className={showAddTaskInput ? classes.show : classes.hide}
placeholder="e.g. Take the dog out for a walk"
inputProps={{ "aria-label": "add task" }}
/>
<div className={classes.controlsWrapper}>
<Button
className={classes.addTaskButton}
disabled={quickTaskDescription.length === 0}
data-testId="quick-add-task-submit"
// onClick={handleAddTaskSubmit}
color="primary"
onClick={handleAddQuickTaskSubmit}
>
Add Task
</Button>
<ProjectTreeWindow />
</div>
</div>
</DialogContent>
</Dialog>
);
};
export default AddQuickTaskDialog;
and for the List component:
import React, { useState } from "react";
import { makeStyles } from "#material-ui/core";
import ListAltTwoToneIcon from "#material-ui/icons/ListAltTwoTone";
import IconButton from "#material-ui/core/IconButton";
import { CustomizedToolTip } from "../index";
import OutlinedInput from "#material-ui/core/OutlinedInput";
import DoneTwoToneIcon from "#material-ui/icons/DoneTwoTone";
import List from "#material-ui/core/List";
import ListItem from "#material-ui/core/ListItem";
import ListItemIcon from "#material-ui/core/ListItemIcon";
import ListItemText from "#material-ui/core/ListItemText";
import FiberManualRecordTwoToneIcon from "#material-ui/icons/FiberManualRecordTwoTone";
import { useProjectsValue, useSelectedProjectValue } from "../../context";
const useStyles = makeStyles(theme => ({
root: {
position: "absolute",
zIndex: 9999,
top: 200,
left: 0,
display: "flex",
flexDirection: "column",
"&:hover $child": {
visibility: "visible"
}
},
wrapper: {
position: "relative !important"
},
selected: {
"& $child": {
visibility: "visible !important"
}
},
hidden: {
visibility: "hidden"
},
listItemIcon: {
minWidth: 30
}
}));
const ProjectTreeWindowList = props => {
const [textInputRef, setTextInputRef] = useState(null);
const [typeProject, setTypedProject] = useState("");
const classes = useStyles(props);
const { projects } = useProjectsValue();
return (
<div className={classes.root}>
<OutlinedInput
// onChange={handleChangeQuickTaskDescription}
inputRef={input => {
setTextInputRef(input);
return input && input.focus();
}}
placeholder="Type a project"
inputProps={{ "aria-label": "select project" }}
/>
<List>
{projects &&
projects.map((project, index) => (
<ListItem
onClick={() => {
alert("move selected project to input");
}}
// selected={active === project.projectId}
button
// classes={{
// root: classes.root,
// selected: classes.selected
// }}
>
<ListItemIcon
className={classes.listItemIcon}
style={{ color: project.color }}
>
<FiberManualRecordTwoToneIcon />
</ListItemIcon>
<ListItemText primary={project.name} />
<ListItemIcon
className={`${classes.listItemIcon} ${classes.hidden}`}
>
<DoneTwoToneIcon />
</ListItemIcon>
</ListItem>
))}
</List>
</div>
);
};
const ProjectTreeWindow = props => {
const classes = useStyles(props);
const [showProjectTreeWindow, setShowProjectTreeWindow] = useState(false);
const handleShowProjectWindow = () => {
setShowProjectTreeWindow(!showProjectTreeWindow);
};
const handleCloseProjectWindow = () => {
setShowProjectTreeWindow(false);
};
return (
<div className={classes.wrapper}>
<CustomizedToolTip title="Select a project">
<IconButton onClick={handleShowProjectWindow} aria-label="add-project">
<ListAltTwoToneIcon />
</IconButton>
</CustomizedToolTip>
{showProjectTreeWindow ? <ProjectTreeWindowList /> : null}
</div>
);
};
export default ProjectTreeWindow;
It is because of the combinaison of position: relative and overflow-y: auto from the <Paper> component. If you override one of this property, it won't be hidden anymore.
To do this, there is a PaperProps property in <Dialog>.
Example:
<Dialog {...otherProps} PaperProps={{style: {position: 'static'} }}/>
I have written a website which you can see here: https://konekto.world/
After the onboarding you will notice that the size of the nearly white outer box is different on every screen (especially at https://konekto.world/emergency_details). I want to have a fixed height for the box (which I might even want to make dependent on the screen size). Could you help me where and how in my code I could make the code the same size. What I did until now has the following effect: https://konekto-k8x5umx6o.now.sh
Emergencydetails/index.js
import React from 'react';
import axios from 'axios';
import { withRouter } from 'react-router-dom';
import { withStyles } from '#material-ui/core/styles';
import { Container, Grid } from '#material-ui/core';
import { Header } from '../Layout';
import FormPersonType from './FormPersonType';
import FormEmergencyType from './FormEmergencyType';
import Textbox from './Textbox';
import AppContext from '../utils/AppContext';
import CONST from '../utils/Constants';
import ProgressiveMobileStepper from './ProgressiveMobileStepper';
const styles = theme => ({
containerWhenIPhone: {
alignItems: 'center',
height: '515.5px',
//width: '414.4px',
maxWidth: 'sm',
border: 'black',
'border-width': 'medium',
'margin-top': '50px',
background: 'rgba(255, 255, 255, 0.8)',
'border-radius': '20px'
},
container: {
alignItems: 'center',
height: '60%',
border: 'black',
'border-width': 'medium',
'margin-top': '50px',
background: 'rgba(255, 255, 255, 0.8)',
'border-radius': '20px'
},
item: {
width: '100%',
'text-align': 'center',
'border-radius': '5px',
'margin-top': '5px',
'justify-content': 'center'
},
container2: {
border: 'black',
'border-width': 'medium',
'margin-top': '30px'
},
picture: { display: 'block', margin: '0 auto' },
box: { width: '230px' }
});
class SOS extends React.Component {
static contextType = AppContext;
constructor(props) {
super(props);
this.state = {
timerOn: false,
componentType: 'type_of_emergency', //type_of_person //texbox
ambulance: false,
fire_service: false,
police: false,
car_service: false,
meAffected: false,
anotherPerson: false,
activeStep: 0
};
this.classes = props.classes;
this.handleNext = this.handleNext.bind(this);
this.handleBack = this.handleBack.bind(this);
this.handleEmergencyType = this.handleEmergencyType.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
showSettings(event) {
event.preventDefault();
}
handleNext(e) {
if (this.state.componentType === 'type_of_emergency') {
this.setState({ componentType: 'type_of_person' });
} else if (this.state.componentType === 'type_of_person')
this.setState({ componentType: 'textbox' });
else if (this.state.componentType === 'textbox') {
this.props.history.push('/transmitted_data');
}
this.setState({ activeStep: this.state.activeStep + 1 });
}
handleBack(e) {
if (this.state.componentType === 'textbox') {
this.setState({ componentType: 'type_of_person' });
} else if (this.state.componentType === 'type_of_person') {
this.setState({ componentType: 'type_of_emergency' });
} else if (this.state.componentType === 'type_of_emergency') {
this.props.history.push('/emergency_sent');
}
this.setState({ activeStep: this.state.activeStep - 1 });
}
handleEmergencyType(new_emergency_state) {
console.log(new_emergency_state);
this.setState(new_emergency_state);
}
onSubmit(e) {
console.log('in OnSubmit');
axios
.post(CONST.URL + 'emergency/create', {
id: 1,
data: this.state
})
.then(res => {
console.log(res);
console.log(res.data);
})
.catch(err => {
console.log(err);
});
}
render() {
let component;
if (this.state.componentType === 'type_of_emergency') {
component = (
<FormEmergencyType
handleComponentType={this.handleComponentType}
handleEmergencyType={this.handleEmergencyType}
emergencyTypes={this.state}
timerStart={this.timerStart}
onSubmit={this.onSubmit}
/>
);
} else if (this.state.componentType === 'type_of_person') {
component = (
<FormPersonType
handleComponentType={this.handleComponentType}
personTypes={this.state}
/>
);
} else if (this.state.componentType === 'textbox') {
component = <Textbox handleFinished={this.handleFinished} />;
}
return (
<React.Fragment>
<Header title="Specify Details" BackButton="true" />
<Container
component="main"
className={this.classes.containerWhenIPhone}
>
<Grid
container
className={this.classes.container}
direction="column"
spacing={2}
>
<Grid item sm={12} className={this.classes.item}>
{component}
</Grid>
</Grid>
<Grid
container
className={this.classes.container2}
direction="column"
spacing={2}
>
<Grid item sm={12} className={this.classes.item}>
<ProgressiveMobileStepper
handleNext={this.handleNext}
handleBack={this.handleBack}
activeStep={this.state.activeStep}
/>
</Grid>
</Grid>
</Container>
</React.Fragment>
);
}
}
export default withRouter(withStyles(styles)(SOS));
// <Container component="main" maxWidth="sm">
I conditionally render the FormPersonType, FormEmergencyType, and Textbox but they donĀ“t contain any styling.
Thank you for your help!
Add the min-height: 60vh, instead of height: 60%, that will work with the fixed height.
also for the container-child fix:
.containerWhenIPhone{
overflow: auto;
box-sizing: content-box;
}
I can't get correct value into the store when trying to upload a file. Instead of file content, I get something like { 0: {} }.
Here's the code:
const renderInput = field => (
<div>
<input {...field.input} type={field.type}/>
{
field.meta.touched &&
field.meta.error &&
<span className={styles.error}>{field.meta.error}</span>
}
</div>
);
render() {
...
<form className={styles.form} onSubmit={handleSubmit(submit)}>
<div className={styles.interface}>
<label>userpic</label>
<Field
name="userpic"
component={renderInput}
type="file"
/>
</div>
<div>
<button type="submit" disabled={submitting}>Submit</button>
<div>
</form>
...
}
All the examples on the web that I found were made using v5 of redux-form.
How do I do file input in redux-form v6?
Create a Field Component like:
import React, {Component} from 'react'
export default class FieldFileInput extends Component{
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this)
}
onChange(e) {
const { input: { onChange } } = this.props
onChange(e.target.files[0])
}
render(){
const { input: { value } } = this.props
const {input,label, required, meta, } = this.props //whatever props you send to the component from redux-form Field
return(
<div><label>{label}</label>
<div>
<input
type='file'
accept='.jpg, .png, .jpeg'
onChange={this.onChange}
/>
</div>
</div>
)
}
}
Pass this component to the Field component where you needed. No need of additional Dropzone or other libraries if you are after a simple file upload functionality.
My example of redux form input wrapper with Dropzone
import React, {Component, PropTypes} from 'react';
import Dropzone from 'react-dropzone';
import { Form } from 'elements';
import { Field } from 'redux-form';
class FileInput extends Component {
static propTypes = {
dropzone_options: PropTypes.object,
meta: PropTypes.object,
label: PropTypes.string,
classNameLabel: PropTypes.string,
input: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node,
cbFunction: PropTypes.func,
};
static defaultProps = {
className: '',
cbFunction: () => {},
};
render() {
const { className, input: { onChange }, dropzone_options, meta: { error, touched }, label, classNameLabel, children, name, cbFunction } = this.props;
return (
<div className={`${className}` + (error && touched ? ' has-error ' : '')}>
{label && <p className={classNameLabel || ''}>{label}</p>}
<Dropzone
{...dropzone_options}
onDrop={(f) => {
cbFunction(f);
return onChange(f);
}}
className="dropzone-input"
name={name}
>
{children}
</Dropzone>
{error && touched ? error : ''}
</div>
);
}
}
export default props => <Field {...props} component={FileInput} />;
Hot to use it:
<FileInput
name="add_photo"
label="Others:"
classNameLabel="file-input-label"
className="file-input"
dropzone_options={{
multiple: false,
accept: 'image/*'
}}
>
<span>Add more</span>
</FileInput>
Another way to do it that will render a preview image (the below example uses React 16+ syntax and only accepts a single image file to send to an API; however, with some minor tweaks, it can also scale to multiple images and other fields inputs):
Working example: https://codesandbox.io/s/m58q8l054x
Working example (outdated): https://codesandbox.io/s/8kywn8q9xl
Before:
After:
containers/UploadForm.js
import React, { Component } from "react";
import { Form, Field, reduxForm } from "redux-form";
import DropZoneField from "../components/dropzoneField";
const imageIsRequired = value => (!value ? "Required" : undefined);
class UploadImageForm extends Component {
state = { imageFile: [] };
handleFormSubmit = formProps => {
const fd = new FormData();
fd.append("imageFile", formProps.imageToUpload.file);
// append any additional Redux form fields
// create an AJAX request here with the created formData
alert(JSON.stringify(formProps, null, 4));
};
handleOnDrop = (newImageFile, onChange) => {
const imageFile = {
file: newImageFile[0],
name: newImageFile[0].name,
preview: URL.createObjectURL(newImageFile[0]),
size: newImageFile[0].size
};
this.setState({ imageFile: [imageFile] }, () => onChange(imageFile));
};
resetForm = () => this.setState({ imageFile: [] }, () => this.props.reset());
render = () => (
<div className="app-container">
<h1 className="title">Upload An Image</h1>
<hr />
<Form onSubmit={this.props.handleSubmit(this.handleFormSubmit)}>
<Field
name="imageToUpload"
component={DropZoneField}
type="file"
imagefile={this.state.imageFile}
handleOnDrop={this.handleOnDrop}
validate={[imageIsRequired]}
/>
<button
type="submit"
className="uk-button uk-button-primary uk-button-large"
disabled={this.props.submitting}
>
Submit
</button>
<button
type="button"
className="uk-button uk-button-default uk-button-large"
disabled={this.props.pristine || this.props.submitting}
onClick={this.resetForm}
style={{ float: "right" }}
>
Clear
</button>
</Form>
<div className="clear" />
</div>
);
}
export default reduxForm({ form: "UploadImageForm" })(UploadImageForm);
components/dropzoneField.js
import React from "react";
import PropTypes from "prop-types";
import DropZone from "react-dropzone";
import ImagePreview from "./imagePreview";
import Placeholder from "./placeholder";
import ShowError from "./showError";
const DropZoneField = ({
handleOnDrop,
input: { onChange },
imagefile,
meta: { error, touched }
}) => (
<div className="preview-container">
<DropZone
accept="image/jpeg, image/png, image/gif, image/bmp"
className="upload-container"
onDrop={file => handleOnDrop(file, onChange)}
>
{({ getRootProps, getInputProps }) =>
imagefile && imagefile.length > 0 ? (
<ImagePreview imagefile={imagefile} />
) : (
<Placeholder
error={error}
touched={touched}
getInputProps={getInputProps}
getRootProps={getRootProps}
/>
)
}
</DropZone>
<ShowError error={error} touched={touched} />
</div>
);
DropZoneField.propTypes = {
error: PropTypes.string,
handleOnDrop: PropTypes.func.isRequired,
imagefile: PropTypes.arrayOf(
PropTypes.shape({
file: PropTypes.file,
name: PropTypes.string,
preview: PropTypes.string,
size: PropTypes.number
})
),
label: PropTypes.string,
onChange: PropTypes.func,
touched: PropTypes.bool
};
export default DropZoneField;
components/imagePreview.js
import React from "react";
import PropTypes from "prop-types";
const ImagePreview = ({ imagefile }) =>
imagefile.map(({ name, preview, size }) => (
<div key={name} className="render-preview">
<div className="image-container">
<img src={preview} alt={name} />
</div>
<div className="details">
{name} - {(size / 1024000).toFixed(2)}MB
</div>
</div>
));
ImagePreview.propTypes = {
imagefile: PropTypes.arrayOf(
PropTypes.shape({
file: PropTypes.file,
name: PropTypes.string,
preview: PropTypes.string,
size: PropTypes.number
})
)
};
export default ImagePreview;
components/placeholder.js
import React from "react";
import PropTypes from "prop-types";
import { MdCloudUpload } from "react-icons/md";
const Placeholder = ({ getInputProps, getRootProps, error, touched }) => (
<div
{...getRootProps()}
className={`placeholder-preview ${error && touched ? "has-error" : ""}`}
>
<input {...getInputProps()} />
<MdCloudUpload style={{ fontSize: 100, paddingTop: 85 }} />
<p>Click or drag image file to this area to upload.</p>
</div>
);
Placeholder.propTypes = {
error: PropTypes.string,
getInputProps: PropTypes.func.isRequired,
getRootProps: PropTypes.func.isRequired,
touched: PropTypes.bool
};
export default Placeholder;
components/showError.js
import React from "react";
import PropTypes from "prop-types";
import { MdInfoOutline } from "react-icons/md";
const ShowError = ({ error, touched }) =>
touched && error ? (
<div className="error">
<MdInfoOutline
style={{ position: "relative", top: -2, marginRight: 2 }}
/>
{error}
</div>
) : null;
ShowError.propTypes = {
error: PropTypes.string,
touched: PropTypes.bool
};
export default ShowError;
styles.css
img {
max-height: 240px;
margin: 0 auto;
}
.app-container {
width: 500px;
margin: 30px auto;
}
.clear {
clear: both;
}
.details,
.title {
text-align: center;
}
.error {
margin-top: 4px;
color: red;
}
.has-error {
border: 1px dotted red;
}
.image-container {
align-items: center;
display: flex;
width: 85%;
height: 80%;
float: left;
margin: 15px 10px 10px 37px;
text-align: center;
}
.preview-container {
height: 335px;
width: 100%;
margin-bottom: 40px;
}
.placeholder-preview,
.render-preview {
text-align: center;
background-color: #efebeb;
height: 100%;
width: 100%;
border-radius: 5px;
}
.upload-container {
cursor: pointer;
height: 300px;
}
I managed to do it with redux-form on material-ui wrapping TextField like this:
B4 edit:
After edit:
<Field name="image" component={FileTextField} floatingLabelText={messages.chooseImage} fullWidth={true} />
with component defined as:
const styles = {
button: {
margin: 12
},
exampleImageInput: {
cursor: 'pointer',
position: 'absolute',
top: 0,
bottom: 0,
right: 0,
left: 0,
width: '100%',
opacity: 0
},
FFS:{
position: 'absolute',
lineHeight: '1.5',
top: '38',
transition: 'none',
zIndex: '1',
transform: 'none',
transformOrigin: 'none',
pointerEvents: 'none',
userSelect: 'none',
fontSize: '16',
color: 'rgba(0, 0, 0, 0.8)',
}
};
export const FileTextField = ({
floatingLabelText,
fullWidth,
input,
label,
meta: { touched, error },
...custom })=>{
if (input.value && input.value[0] && input.value[0].name) {
floatingLabelText = input.value[0].name;
}
delete input.value;
return (
<TextField
hintText={label}
fullWidth={fullWidth}
floatingLabelShrinkStyle={styles.FFS}
floatingLabelText={floatingLabelText}
inputStyle={styles.exampleImageInput}
type="file"
errorText={error}
{...input}
{...custom}
/>
)
}
If you need base64 encoding to send it to your backend, here is a modified version that worked for me:
export class FileInput extends React.Component {
getBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
onFileChange = async (e) => {
const { input } = this.props
const targetFile = e.target.files[0]
if (targetFile) {
const val = await this.getBase64(targetFile)
input.onChange(val)
} else {
input.onChange(null)
}
}
render() {
return (
<input
type="file"
onChange={this.onFileChange}
/>
)
}
}
Then your field component would look like:
<Field component={FileInput} name="primary_image" type="file" />
For React >= 16 and ReduxForm >= 8 (tested version are 16.8.6 for React and 8.2.5)
works following component.
(Solution posted in related GitHub issue by DarkBitz)
const adaptFileEventToValue = delegate => e => delegate(e.target.files[0]);
const FileInput = ({
input: { value: omitValue, onChange, onBlur, ...inputProps },
meta: omitMeta,
...props
}) => {
return (
<input
onChange={adaptFileEventToValue(onChange)}
onBlur={adaptFileEventToValue(onBlur)}
type="file"
{...props.input}
{...props}
/>
);
};
export const FileUpload = (props) => {
const { handleSubmit } = props;
const onFormSubmit = (data) => {
console.log(data);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<div>
<label>Attachment</label>
<Field name="attachment" component={FileInput} type="file"/>
</div>
<button type="submit">Submit</button>
</form>
)
}
With Redux Form
const { handleSubmit } = props;
//make a const file to hold the file prop.
const file = useRef();
// create a function to replace the redux-form input-file value to custom value.
const fileUpload = () => {
// jsx to take file input
// on change store the files /file[0] to file variable
return (
<div className='file-upload'>
<input
type='file'
id='file-input'
accept='.png'
onChange={(ev) => {
file.current = ev.target.files;
}}
required
/>
</div>
);
};
//catch the redux-form values!
//loop through the files and add into formdata
//form data takes key and value
//enter the key name as multer-config fieldname
//then add remaining data into the formdata
//make a request and send data.
const onSubmitFormValues = (formValues) => {
const data = new FormData();
for (let i = 0; i < file.current.length; i++) {
data.append("categoryImage", file.current[i]);
}
data.append("categoryName", formValues.categoryName);
Axios.post("http://localhost:8080/api/v1/dev/addNewCategory", data)
.then((response) => console.log(response))
.catch((err) => console.log(err));
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You can also use react-dropzone for this purpose. The below code worked fine for me
filecomponent.js
import React from 'react'
import { useDropzone } from 'react-dropzone'
function MyDropzone(props) {
const onDrop = (filesToUpload) => {
return props.input.onChange(filesToUpload[0]);
}
const onChange = (filesToUpload) => {
return props.input.onChange(filesToUpload[0]);
}
const { getRootProps, getInputProps } = useDropzone({ onDrop });
return (
<div {...getRootProps()}>
<input {...getInputProps()} onChange={e => onChange(e.target.files)} />
<p> Drop or select yout file</p>
</div>
)
}
export default MyDropzone;
In form use this
<Field
name="myfile"
component={renderFile}
/>