Next.js not re-rendering UI on state change - css

On my Next js project I am looping through an array of amenities that displays a div that when clicked toggles an active prop. The addAmenity function handles the logic that loops through the amenities array and toggles the specific array item's active property. If the active prop of the div is true the .amenities-active class is supposed to be applied to it and the background of the div should turn green but it does not. Is there any idea as to what I am doing wrong. The console.log(tempList) confirms a change to true when clicked on a false amenity but the UI does not change to have a green background color.
//Next js
const amenitiesDefaultArr = [
{
data: "bathroom",
text: "Private Bathroom",
icon: <WcIcon fontSize="large" />,
active: false,
},
{
data: "dining",
text: "Dining Hall",
icon: <FastfoodIcon fontSize="large" />,
active: false,
},
{
data: "wifi",
text: "Wifi",
icon: <WifiIcon fontSize="large" />,
active: false,
}
]
const addAmenity = (e) => {
let dataItem = e.currentTarget.dataset.amenity
let tempList = amenitiesList
tempList.map(el => {
if (el.data === dataItem) el.active = !el.active
return el
})
console.log(tempList)
setAmenitiesList(tempList)
}
const AddDorm = () => {
const [amenitiesList, setAmenitiesList] = useState(amenitiesDefaultArr)
return (
<>
{
amenitiesList.map(el => {
const {data, text, icon } = el
let { active } = el
return (
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
data-amenity={data}
onClick={(e) => addAmenity(e)}
>
<p>{text}</p>
{icon}
</div>
)
</>
})
)
/* CSS */
.amenity {
padding: 0.5rem 1rem;
display: flex;
align-items: center;
border-radius: 50px;
box-shadow: 5px 5px 10px #919191,
-5px -5px 10px #ffffff;
z-index: 4;
cursor: pointer;
}
.amenity-active {
background-color: var(--green);
}

The main problem is you are trying to pass data in a JSX element as you would do in html.
data-amenity={data}
React does work this way.So, e.currentTarget.dataset.amenity is always undeined.Instead, React uses refs to access dom elements. You can learn more about refs in the official React documentation. But in your case, you don't even need any ref as you can send data directly to any function. Check:
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
// data-amenity={data}
onClick={() => addAmenity(data)}
>
<p>{text}</p>
</div>
and in addAmenity just receive it
const addAmenity = (incoming) => {
let dataItem = incoming
...
}
Below I provide the version of your code I fixed for you which is working perfect. Please let me know if this was helpful.
//Next js
import { useState } from 'react'
const amenitiesDefaultArr = [
{
data: "bathroom",
text: "Private Bathroom",
icon: <WcIcon fontSize="large" />,
active: false,
},
{
data: "dining",
text: "Dining Hall",
icon: <FastfoodIcon fontSize="large" />,
active: false,
},
{
data: "wifi",
text: "Wifi",
icon: <WifiIcon fontSize="large" />,
active: false,
}
]
const AddDorm = () => {
const [amenitiesList, setAmenitiesList] = useState(amenitiesDefaultArr)
const addAmenity = (incoming) => {
let dataItem = incoming
const tempList = amenitiesList.map(el => {
if (el.data === dataItem) el.active = !el.active
return el
})
console.log(tempList)
setAmenitiesList(tempList)
}
return (
<>
{
amenitiesList.map(el => {
const {data, text, icon} = el
let { active } = el
return (
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
// data-amenity={data}
onClick={() => addAmenity(data)}
>
<p>{text}</p>
{icon}
</div>
)})}
</>
)
}
export default AddDorm

Related

onClick on stacked components

I have two react arrow function components stacked on top of each other (using absolute positioning) and both of them have onClick attributes. The problem is, I want to click on the one that is on top, and both onClick functions trigger. Is there anyway to work around this?
This is a simplified version of the code:
const Card = ({...}) => {
const styles = {
optionsButton: {
minWidth:0,
minHeight: 0,
padding: "2px",
position: "absolute",
color: "#808080",
zIndex: 1,
right: 5,
top: 5,
'&:hover':{
backgroundColor: 'rgb(0, 0, 0, 0.1)'
}
},
}
const [hovering, setHovering] = useState(false)
const [isCardExpanded, setIsCardExpanded] = useState(false)
const expandCard = () => {
setIsCardExpanded(true)
}
const closeCard = () => {
setIsCardExpanded(false)
}
const mainPaperStyle = () => {
let style = {
padding: "10px",
cursor: "pointer",
position: "absolute",
"&:hover": {
filter: "brightness(97%)"
}
}
//Extra code here modifying color of the style, no positioning modifications
return style
}
const buttonAction = () => {
console.log("Do action!")
}
return(
<>
<Paper sx={mainPaperStyle()} onClick={expandCard} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)}>
Lorem Ipsum
{hovering &&
<Button variant="filled"
id="card-button"
sx={styles.optionsButton}
onClick={() => buttonAction()}>
<MoreVertIcon/>
</Button>
}
</Paper>
</>
)
}
And here is a screenshot of why I want two components stacked on top of each other:
Before hovering:
After hovering:
I want a Button to appear when hovering on top of the Paper component. The problem is, when I click the button, both expandCard and buttonAction trigger. (I am using Material UI btw)
You can use $event.stopPropagation();.
const firstFn = () => { // first function body };
const secondFn = (event: MouseEventHandler<HTMLButtonElement>) => {
$event.stopPropagation();
// second function body
}
So in your case you need to change function buttonAction to this
const buttonAction = (event) => {
$event.stopPropagation();
console.log("Do action!")
}
and return clause with
return(
<>
<Paper sx={mainPaperStyle()} onClick={expandCard} onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)}>
Lorem Ipsum
{hovering &&
<Button variant="filled"
id="card-button"
sx={styles.optionsButton}
onClick={() => buttonAction($event)}>
<MoreVertIcon/>
</Button>
}
</Paper>
</>
)
You can learn about this more in here

Applying CSS classes to a dynamic collection of React nodes on a consistent delay

I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}

Change color of bottom border and dropdown arrow in Material UI Autocomplete

I want to make the line underneath 'Search' and the arrow on the right white but I can't figure out how to do it for the life of me. I've tried using styled on the .MuiAutocomplete-root css class but it didn't work. I can't figure out which CSS class to apply the color to. If I inspect it, it says that the class is MuiInput-root which I also tried with styled and that didn't work either.
Thanks
My code (copy pasted from the docs with some minor adjustments):
function sleep(delay = 0) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
});
}
export default function AutocompleteSearch() {
const [open, setOpen] = useState(false);
const [options, setOptions] = useState([]);
const loading = open && options.length === 0;
useEffect(() => {
let active = true;
if (!loading) {
return undefined;
}
(async () => {
await sleep(1e3); // For demo purposes.
if (active) {
//api call then setOptions
}
})();
return () => {
active = false;
};
}, [loading]);
useEffect(() => {
if (!open) {
setOptions([]);
}
}, [open]);
return (
<Autocomplete
id="size-small-standard"
size="small"
sx={{
width: 300,
}}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
isOptionEqualToValue={(option, value) => option.title === value.title}
getOptionLabel={(option) => option.title}
options={options}
groupBy={(option) => option.type}
loading={loading}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
label="Search"
//makes label white
InputLabelProps={{
style: {color: '#fff'},
}}
InputProps={{
...params.InputProps,
//makes the selected option white when added to the box
sx: {color: '#fff'},
endAdornment: (
<>
{loading ? <CircularProgress color="inherit" size={20}/> : null}
{params.InputProps.endAdornment}
</>
),
}}
/>
)}
/>
);
}
Add color to the following CSS classes.
.MuiSvgIcon-root {
color: white;
}
.css-ghsjzk-MuiInputBase-root-MuiInput-root:before {
border-bottom-color: white !important;
}
.css-ghsjzk-MuiInputBase-root-MuiInput-root:after {
border-bottom-color: white !important;
}
Play around with the code here
I used red color in my codesandbox example so that it can be visible on white screen

Drag and Drop in a scaled container with react-dnd

I've almost got this working in a codesandbox, but the drop accuracy still isn't quite right. I'm at a loss for what else to try.
Here is my container:
import update from "immutability-helper";
import { useCallback, useState } from "react";
import { useDrop } from "react-dnd";
import { DraggableBox } from "./DraggableBox.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid as doSnapToGrid } from "./snapToGrid.js";
const styles = {
width: 300,
height: 300,
border: "1px solid black",
position: "relative",
transform: "scale(.8)"
};
export const Container = ({ snapToGrid }) => {
const [boxes, setBoxes] = useState({
a: { top: 20, left: 80, title: "Drag me around" },
b: { top: 180, left: 20, title: "Drag me too" }
});
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top }
}
})
);
},
[boxes]
);
const [, drop] = useDrop(
() => ({
accept: ItemTypes.BOX,
drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let left = item.left + delta.x;
let top = item.top + delta.y;
if (snapToGrid) {
[left, top] = doSnapToGrid(left, top);
}
moveBox(item.id, left, top);
return undefined;
}
}),
[moveBox]
);
return (
<div ref={drop} style={styles}>
{Object.keys(boxes).map((key) => (
<DraggableBox key={key} id={key} {...boxes[key]} />
))}
</div>
);
};
Here is my draggable box:
import { memo, useEffect } from 'react'
import { useDrag } from 'react-dnd'
import { getEmptyImage } from 'react-dnd-html5-backend'
import { Box } from './Box.js'
import { ItemTypes } from './ItemTypes.js'
function getStyles(left, top, isDragging) {
const transform = `translate3d(${left }px, ${top }px, 0)`
return {
position: 'absolute',
transform,
WebkitTransform: transform,
// IE fallback: hide the real node using CSS when dragging
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
height: isDragging ? 0 : '',
}
}
export const DraggableBox = memo(function DraggableBox(props) {
const { id, title, left, top } = props
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
item: { id, left, top, title },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, left, top, title],
)
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true })
}, [])
return (
<div
ref={drag}
style={getStyles(left, top, isDragging)}
role="DraggableBox"
>
<Box title={title} />
</div>
)
})
Here is my custom drag layer:
import { useDragLayer } from "react-dnd";
import { BoxDragPreview } from "./BoxDragPreview.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid } from "./snapToGrid.js";
const layerStyles = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0 ,
top: 0 ,
width: "100%",
height: "100%",
};
function getItemStyles(initialOffset, currentOffset, isSnapToGrid) {
if (!initialOffset || !currentOffset) {
return {
display: 'none',
}
}
let { x, y } = currentOffset
if (isSnapToGrid) {
x -= initialOffset.x
y -= initialOffset.y
;[x, y] = snapToGrid(x, y)
x += initialOffset.x
y += initialOffset.y
}
const transform = ` translate(${x }px, ${y }px)`
return {
transform,
WebkitTransform: transform,
}
}
export const CustomDragLayer = (props) => {
const {
itemType,
isDragging,
item,
initialOffset,
currentOffset,
delta
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
delta: monitor.getDifferenceFromInitialOffset(),
isDragging: monitor.isDragging()
}));
function renderItem() {
switch (itemType) {
case ItemTypes.BOX:
return <BoxDragPreview title={item.title} />;
default:
return null;
}
}
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div style={getItemStyles(initialOffset, currentOffset, props.snapToGrid)}>{renderItem()}</div>
</div>
);
};
And here is my box drag preview:
import { memo, useEffect, useState } from "react";
import { Box } from "./Box.js";
const styles = {
display: "inline-block",
transform: "scale(.8)",
transformOrigin: "top left"
};
export const BoxDragPreview = memo(function BoxDragPreview({ title }) {
const [tickTock, setTickTock] = useState(false);
useEffect(
function subscribeToIntervalTick() {
const interval = setInterval(() => setTickTock(!tickTock), 500);
return () => clearInterval(interval);
},
[tickTock]
);
return (
<div style={styles}>
<Box title={title} yellow={tickTock} preview />
</div>
);
});
I tried item.left + delta.x / scaleValue, but that was even less accurate than this example. Here is the url to the codesandbox.
https://codesandbox.io/s/dazzling-wozniak-2fvs81?file=/src/BoxDragPreview.js:0-616

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