How to dispatch Redux action without "onClick"? - redux

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

Is there any way to use react-moveable in Nextjs?

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?

How to remove lines from Router Link? style={{'textDecoration': 'none'}} is not working

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>

Element positioned absolute inside Dialog Material UI React

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'} }}/>

React: make boxes same size even with different content

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;
}

How to upload file with redux-form?

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}
/>

Resources