React table header width is changing when there is no data (after filtering) - react-table

I am working on a react-table that has sort and filter functionalities. When I filter and there is no data to be returned (empty table), the headers' widths are not staying fixed, they get reduced accordingly to the length of the header title. This is happening because when there is no data, tbody is completely empty, thus there are no cells that the headers can use for defining their widths.
You can see that in the schema when I am rendering each cell, I am assigning it a class. I use that class in the stylesheet to give the cells a specific width. I want all the cells to have a fixed width of 140px, and only the summary cell (task type) to have a fixed width of 360px. This works great for the cells, and when there is data the headers follow the cell widths.
See the picture (I had to cover the data due to privacy + I am showing only one row) - in reality, it has more. You can see in the picture that the header widths are matching the cells' widths.
Now, as soon as I filter the data out, and the filter input does not match any of the data, the headers get reduced to a width that depends on the length of the header titles. See picture:
The best comparison would be the following pictures of both headers placed on top of each other:
I tried many things: from using the useBlock/Absolute/FlexLayout Hooks to assigning the widths in the schema instead of via a CSS class, to playing around with CSS and trying out different options, but so far nothing works.
I would be thankful for any tips and suggestions.
Here is the code for the table UI:
const TaskManagerTable = (props) => {
const {columns, data} = props;
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({
columns,
data,
},
useFilters,
useSortBy,
);
return (
<>
<table className={CLASS_NAMES.TABLE}{...getTableProps()}>
<thead className={CLASS_NAMES.TABLE_HEAD}>
{headerGroups.map(headerGroup => {
const {key, ...restHeaderGroupProps} = headerGroup.getHeaderGroupProps()
return (
<tr className={CLASS_NAMES.HEADER_ROW} key={key} {...restHeaderGroupProps}>
{headerGroup.headers.map(column => {
const {key, ...restColumn} = column.getHeaderProps();
return (
<th className={CLASS_NAMES.HEADER_CELL} key={key} {...restColumn}
style={{
cursor: column.canSort ? 'pointer' : 'auto',
}}
>
{column.render('Header')}
<span className={CLASS_NAMES.SORT} {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.isSorted ?
<div className={CLASS_NAMES.ICON_SORTED}>
<SortIcon dir={column.isSortedDesc ? 'desc' : 'asc'} onClick={_.noop} />
</div>
: column.canSort
? <div className={CLASS_NAMES.ICON_CAN_SORT}>
<SortIcon dir={column.isSortedDesc ? 'desc' : 'asc'} onClick={_.noop} />
</div> : ''
}
</span>
<span className={CLASS_NAMES.FILTER}>
{column.canFilter ? column.render('Filter') : null}
</span>
</th>
)
})}
</tr>
)
})}
</thead>
<tbody {...getTableBodyProps}>
{rows.map(row => {
prepareRow(row)
const {key, ...restRowProps} = row.getRowProps();
return (
<tr key={key} {...restRowProps}>
{row.cells.map(cell => {
const {key, ...restCellProps} = cell.getCellProps();
return (
<td key={key} {...restCellProps}>
{cell.render('Cell')}
</td>
);
})}
</tr>
)
})}
</tbody>
</table>
</>
)
};
TaskManagerTable.propTypes = {
columns: PropTypes.array,
data: PropTypes.array,
};
const mapDispatchToProps = {};
export default connect(null, mapDispatchToProps)(TaskManagerTable);
Here is the schema:
export default [
{
Header: 'ID',
accessor: 'key',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const key = _.get(row, 'original.key');
return (
<div className={CLASS_NAMES.ID_CELL}>
{key}
</div>
)
}
},
{
Header: 'Category',
accessor: 'category',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const category = _.get(row, 'original.category');
return (
<div className={CLASS_NAMES.CATEGORY_CELL}>
{category}
</div>
)
}
},
{
Header: 'Subcategory',
accessor: 'subCategory',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const subCategory = _.get(row, 'original.subCategory');
return (
<div className={CLASS_NAMES.SUBCATEGORY_CELL}>
{subCategory}
</div>
)
}
},
{
Header: 'Task Title',
accessor: 'summary',
className: 'summary',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const summary = _.get(row, 'original.summary');
return (
<div className={CLASS_NAMES.SUMMARY_CELL}>
{summary}
</div>
)
}
},
{
Header: 'Priority',
accessor: 'priority',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const priority = _.get(row, 'original.priority');
return (
<div className={CLASS_NAMES.PRIORITY_CELL}>
{priority}
</div>
)
}
},
{
Header: 'Status',
accessor: 'status',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
},
Cell: ({row}) => {
const status = _.get(row, 'original.status');
return (
<div className={CLASS_NAMES.STATUS_CELL}>
{status}
</div>
)
}
},
{
Header: 'Task Type',
accessor: 'type',
Filter: ({ column }) => {
return (
<ReactTableFilter containerClassName={CLASS_NAMES.TASK_MANAGER_FILTER_CONTAINER}
column={column}
popoverInner={<FreeTextFilter handleChangeFilter={column.setFilter} filter={column.filterValue}/>}
/>
)
}
]
And finally here is the SCSS:
table {
width: 100%;
thead {
position: sticky;
top: 0;
background-color: white;
tr {
text-align: left;
text-transform: uppercase;
color: #91a4c3;
font-weight: 400;
font-size: 12px;
th {
padding-left: 10px;
padding-bottom: 10px;
.sort-container {
margin-left: 25px;
position: absolute;
}
.filter-container {
margin-left: 30px;
position: absolute;
}
.icon-can-sort, .filter-component-hide {
display: none;
}
.filter-component-show {
display: block;
}
&:hover {
.icon-can-sort, .filter-component-hide {
display: block;
}
}
#include relative();
:nth-child(4) {
min-width: 360px;
}
}
}
}
tbody {
overflow-y: auto;
#include scrollBar();
tr {
box-shadow: 0px 1px 5px 0 rgba(104, 105, 111, 0.09);
height: 45px;
color: #6a7097;
text-transform: capitalize;
td {
padding-left: 10px;
}
.id-cell,
.category-cell,
.subcategory-cell,
.priority-cell,
.status-cell,
.type-cell,
{
min-width: 140px;
}
.summary-cell {
min-width: 360px;
}
}
}
}

Related

Nested flex item divs rendered using React not restricting themselves to coequal/correct size?

I am attempting to render a custom Table component in React that will render [ "linear"-looking ] sub-tables, if and only if the values of its object rows prop are themselves of type 'object'. To do this I have my parent Table component, that renders a child TableRow component, that then does the conditional rendering of either a SubTable component or a TableInnerSquare component.
So far it works perfectly for the base condition [ when the properties of the rows object are simple ], but when I try to render sub-tables, the TableRows overflow the width of their container and I can't figure out how to stop this from happening.
Table component:
function Table(props) {
const { rows, columns, tableWidth, rowHeight } = props;
// rows here should be an array of rows containing objects w/ properties keyed by column names
// columns should just be an array of column names
return (
<div className='g-table'
style={{
display: 'flex',
width: tableWidth,
flexDirection: 'column',
margin: '5% auto',
}}
>
<div className='column-id-container'
style={{
display: 'flex',
width: tableWidth,
height: rowHeight,
}}
>
{ columns.map((column,idx) => {
return (
<div className='column-id'
style={{
backgroundColor: 'lightblue',
border: '1px solid blue',
width: '100%',
overflow: 'hidden',
padding: '2%',
}}
key={idx}
>
{ column }
</div>
);
}) }
</div>
<div className='rows-container'>
{ rows.map((row,idx) => {
return (
<TableRow
key={idx}
row={row}
rowId={idx}
tableWidth={tableWidth}
rowHeight={rowHeight}
columns={columns}
/>
);
}) }
</div>
</div>
);
};
TableRow component:
function TableRow(props) {
const { columns, row, rowId, tableWidth, rowHeight } = props;
// row should be an object with keys for each column here;
// columns should be an array
console.log('columns:');
console.log(columns);
console.log('row:');
console.log(row);
return (
<div className='table-row'
style={{
display: 'flex',
width: tableWidth,
}}
>
{ columns.map((property,idx) => {
if (typeof (row[property]) === 'object') {
return (
<SubTable
dataObject={row[property]}
rowHeight={rowHeight} // so for the SubTablesTable the row should be an object of objects
key={idx}
/>
);
} else {
return (
<TableInnerSquare
innerData={row[property]}
rowHeight={rowHeight}
key={idx}
/>
);
}
}) }
</div>
);
}
SubTable:
function SubTable(props) {
const { dataObject, rowHeight } = props;
console.log('dataObject:');
console.log(dataObject);
return (
<div className='sub-table'
style={{
width: 'auto',
display: 'flex',
flex: '1',
}}
>
{ Object.entries(dataObject).map((entry,idx) => {
return (
<div className='sub-table-inner'
style={{
display: 'flex',
overflow: 'hidden',
}}
>
<TableInnerSquare
rowHeight={rowHeight}
innerData={entry[0]}
/>
<TableInnerSquare
rowHeight={rowHeight}
innerData={entry[1]}
/>
</div>
);
}) }
</div>
);
}
TableInnerSquare:
function TableInnerSquare(props) {
const { innerData, rowHeight } = props;
return (
<div
className='table-inner-square'
style={{
backgroundColor: 'gold',
border: '1px solid red',
height: rowHeight,
overflow: 'hidden',
padding: '2%',
width: '100%',
}}
>
{ innerData }
</div>
);
}
Any help figuring out how to restrict TableRows containing SubTables to having width tableWidth [ which like I said the TableRows containing TableInnerSquares already seem to do? ] would be appreciated!
You can set overflow: hidden on a div, but its parent divs will still recognize the content as present and grow to contain it. You need to set overflow: hidden on the div with class sub-table [ in component SubTable ] here, in order for the sub-tables to hide the overflows of their inner divs.

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

React component with state triggers animation in subcomponent when the state of parent component is changed

ProjectsSection component renders a document section with header and cards. A header animation should be triggered when the header is visible on the screen for the 1st time and run only once. However when I added a state to the ProjectsSection component with every state change a header animation runs and what is surprising to me only a part of it (letters/ spans that are children to h2 move, but brackets/ pseudoelements on h2 don't). In my project I use css modules for styling.
I have tried to wrap SectionHeader component in React.memo but it does not help.
Main component:
const ProjectsSection = () => {
const [openProjectCardId, setOpenProjectCardId] = useState('');
return (
<section id="projects" className="section">
<div className="container">
<SectionHeader>Projects</SectionHeader>
<div className={styles.content_wrapper}>
{projects.map((project, ind) => (
<Project
key={project.id}
project={project}
ind={ind}
isOpen={openProjectCardId === project.id}
setOpenProjectCardId={setOpenProjectCardId}
/>
))}
</div>
</div>
</section>
);
};
SectionHeader component:
const SectionHeader = ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
);
const textToLetters = children.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{textToLetters}</h2>
</div>
);
};
Css
.sectionHeader_wrapper {
position: relative;
// other properties
&::before {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
&::after {
display: block;
content: ' ';
position: absolute;
opacity: 0;
// other properties
}
}
.sectionHeader {
position: relative;
display: inline-block;
overflow: hidden;
// other properties
}
.letter {
position: relative;
display: inline-block;
transform: translateY(100%) skew(0deg, 20deg);
}
.sectionHeader__isVisible .letter {
animation: typeletter .25s ease-out forwards;
}
useIntersection hook
const useIntersection = (
activeClass,
{ root = null, rootMargin = '0px', threshold = 1 },
dependency = [],
unobserveAfterFirstIntersection = true
) => {
const elementRef = useRef(null);
useEffect(() => {
const options: IntersectionObserverInit = {
root,
rootMargin,
threshold,
};
const observer = new IntersectionObserver((entries, observerObj) => {
entries.forEach((entry) => {
if (unobserveAfterFirstIntersection) {
if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
observerObj.unobserve(entry.target);
}
} else if (entry.isIntersecting) {
entry.target.classList.add(activeClass);
} else {
entry.target.classList.remove(activeClass);
}
});
}, options);
// if (!elementRef.current) return;
if (elementRef.current) {
observer.observe(elementRef.current);
}
}, [...dependency]);
return elementRef;
};
I have found a solution to this problem.
This was apparently about textToLetters variable created every time the state of parent component was changing.
I used useMemo to keep this value and now animation is not triggered anymore.
const textToLetters = (text) =>
text.split('').map((letter) => {
const style = !letter ? styles.space : styles.letter;
return (
<span className={style} key={nanoid()}>
{letter}
</span>
);
});
const SectionHeader= ({ children }) => {
const headerRef = useIntersection(
styles.sectionHeader__isVisible,
{ rootMargin: '0px 0px -100px 0px', threshold: 1 },
[children]
);
const letters = React.useMemo(() => textToLetters(children), [children]);
return (
<div className={styles.sectionHeader_wrapper} ref={headerRef}>
<h2 className={styles.sectionHeader}>{letters}</h2>
</div>
);
};

How to display three post photo div at the bottom of the page using Reactjs and Css

The code below has three posts photos in the array. When I click on each post button am supposed to be seeing
Three post Photo div that corresponds to each post all at the bottom.
My Problem:
My issue is that it is showing just one post photo div which keeps replacing others after I added the CSS code below.
const mainArea={
position: 'fixed',
width: '80%',
bottom: '0%',
display: 'inline-block'
}
const photodiv={
position: 'relative',
width: '250px',
// height:auto,
background: 'orange',
color: 'black',
borderRadius: '5px 5px 0px 0px',
bottom: '0px',
}
screenshot showing jammed div based on the CSS implementation
What I want:
I need to be seeing three div post photos if the three toggle button is clicked
Here is the main code
import React, { Component, Fragment } from "react";
import { render } from "react-dom";
const mainArea={
position: 'fixed',
width: '80%',
bottom: '0%',
display: 'inline-block'
}
const photodiv={
position: 'relative',
width: '250px',
// height:auto,
background: 'orange',
color: 'black',
borderRadius: '5px 5px 0px 0px',
bottom: '0px',
}
class Focus extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
shown: true,
};
}
componentDidMount() {
this.setState({
data: [
{ id: "1", title: "my first title", image: "http://localhost/apidb_react/1.png", visible: true , photoVisible: true},
{ id: "2", title: "my second title", image: "http://localhost/apidb_react/2.png", visible: true, photoVisible: true},
{ id: "3", title: "my third title", image: "http://localhost/apidb_react/3.png", visible: true, photoVisible: true}
]
});
}
toggle(id) {
const newData = this.state.data.map(item => {
if(item.id === id) {
return { ...item, visible: !item.visible};
}
return item;
})
this.setState({
data: newData
});
}
/*
hideUnhidePhoto(id) {
const newData = this.state.data.map(item => {
alert(id);
if(item.id === id) {
alert('ttto ' +item.id);
return { ...item, photoVisible: !item.photoVisible};
}
return item;
})
this.setState({
data: newData
});
}
*/
hideUnhidePhoto(id) {
this.setState(({ data }) => {
return {
data : data.map(item => ({
...item,
photoVisible : (id == item.id) ? !item.photoVisible : item.photoVisible }))
}
});
}
render() {
return (
<div>
<label>
<ul>
{this.state.data.map((post, i) => (
<li key={i}>
<div style={mainArea}>
<div style={photodiv}>
<div style={{ display: post.visible ? "none" : "block"}}>
<b>Post Data:</b> {post.title} --{post.id} <br />
<span style={{color: 'red'}} onClick={ () => this.hideUnhidePhoto(post.id) }> Hide/Unhide Photo</span>
<div style={{ display: post.photoVisible ? "block" : "none"}}>
<img src={post.image} />
</div>
</div></div>
</div>
<button onMouseDown={ () => this.toggle(post.id) }>Toggle </button><br />
<br />
</li>
))}
</ul>
</label>
</div>
);
}
}
If I understand the issue correctly, then this can be fixed by adjusting your mainArea style object like so:
const mainArea={
/* position: 'fixed', Remove position fixed */
width: '80%',
bottom: '0%',
display: 'inline-block'
}
The fixed position basically has the effect of placing elements at a location on screen, relative to the client area of the window. This means that if you have multiple elements that share the same (default) coordinates, and are positioned with the fixed rule, then those elements will effectively overlap one another. This gives the appearance of only one element being visible at any given time.
For a working example, see this jsFiddle:
enter link description here
Hope that helps!

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