Wordpress Gutenberg block - wordpress

I have a problem with a custom block which send me an error when I reload the edition page.
I don't understand what the problem is. In regards of the error, actual and expected are the same.
Here the error :
Block validation: Block validation failed for namespace/nottomiss ({object}).
Expected:
<div class="wp-block-utopiales-nottomiss"><p>label test</p><p>label test</p></div>
Actual:
<div class="wp-block-utopiales-nottomiss"><p>label test</p><p>title test</p></div>
Here my code :
const { registerBlockType } = wp.blocks;
const { __ } = wp.i18n;
const { PanelBody, TextControl } = wp.components;
const { BlockControls, InspectorControls, RichText } = wp.editor;
const { createElement, Fragment } = wp.element
registerBlockType( 'namespace/nottomiss', {
title: __( 'Nottomiss' ),
description: __('My description'),
icon: 'star-filled',
category: 'widgets',
supports: { align: true, alignWide: true },
attributes: {
label: {
type: 'string',
source: 'html',
selector: 'p',
},
title: {
type: 'string',
source: 'html',
selector: 'p',
},
},
edit: function( props ) {
const { label, title } = props.attributes;
function onChangeLabel( newLabel ) {
props.setAttributes( { label: newLabel } );
}
function onChangeTitle( newTitle ) {
props.setAttributes( { title: newTitle } );
}
return (
<Fragment>
<BlockControls>
</BlockControls>
<InspectorControls>
<PanelBody title={ __( 'List' ) }>
</PanelBody>
</InspectorControls>
<RichText
identifier="label"
tagName="p"
placeholder=""
value={ label }
onChange={ onChangeLabel }
/>
<RichText
identifier="title"
tagName="p"
placeholder=""
value={ title }
onChange={ onChangeTitle }
/>
</Fragment>
);
},
save: function( props ) {
const { label, title } = props.attributes;
return (
<div>
<RichText.Content
tagName="p"
value={ label }
/>
<RichText.Content
tagName="p"
value={ title }
/>
</div>
);
},
} );
Thanks in advance for your answer,

The selectors are how the editor pulls the data from the saved html, and currently your selectors aren't targeting the content. You could change your selectors to something like this:
attributes: {
label: {
type: 'string',
source: 'html',
selector: '.label'
},
title: {
type: 'string',
source: 'html',
selector: '.title'
}
}
And you could change your save function to this:
save: function(props) {
const { label, title } = props.attributes
return (
<div>
<div className="label">
<RichText.Content
value={ label }
/>
</div>
<div className="title">
<RichText.Content
value={ title }
/>
</div>
</div>
)
}

Related

Font awesome icon in Vue.js does not display

I am trying to add a font-awesome arrow icon via my css code like this:
<style>
.negative {
color: red;
}
.positive {
color: green;
}
.negative::after {
content: "\f106";
}
</style>
I have font-awesome included in my html via CDN. For some reason my icon does not display properly, it just shows a square. Any ideas why and how I can fix it?
Here is the rest of my code, showing the logic behind the displaying of percentages:
<template>
<div>
<v-data-table
:headers="headers"
:items="rowsToDisplay"
:hide-default-footer="true"
class="primary"
>
<template #item.thirtyDaysDiff="{ item }">
<span :class="item.thirtyDaysDiffClass">{{ item.thirtyDaysDiff }}%</span>
</template>
<template #item.sevenDaysDifference="{ item }">
<span :class="item.sevenDaysDiffClass">{{ item.sevenDaysDiff }}%</span>
</template>
</v-data-table>
</div>
</template>
<script>
import axios from 'axios';
export default {
data () {
return {
bitcoinInfo: [],
isPositive: false,
isNegative: false,
headers: [
{
text: 'Currency',
align: 'start',
value: 'currency',
},
{ text: '30 Days Ago', value: '30d' },
{ text: '30 Day Diff %', value: 'thirtyDaysDiff'},
{ text: '7 Days Ago', value: '7d' },
{ text: '7 Day Diff %', value: 'sevenDaysDifference' },
{ text: '24 Hours Ago', value: '24h' },
],
}
},
methods: {
getBitcoinData() {
axios
.get('data.json')
.then((response => {
var convertedCollection = Object.keys(response.data).map(key => {
return {currency: key, thirtyDaysDiff: 0, sevenDaysDifference: 0, ...response.data[key]}
})
this.bitcoinInfo = convertedCollection
}))
.catch(err => console.log(err))
},
calculateDifference(a, b) {
let calculatedPercent = 100 * Math.abs((a - b) / ((a + b) / 2));
return Math.max(Math.round(calculatedPercent * 10) / 10, 2.8).toFixed(2);
},
getDiffClass(a, b) {
return a > b ? 'positive': a < b ? 'negative' : ''
},
calculateSevenDayDifference(item) {
let calculatedPercent = 100 * Math.abs((item['24h'] - item['7d']) / ((item['24h'] + item['7d']) / 2));
return Math.max(Math.round(calculatedPercent * 10) / 10, 2.8).toFixed(2);
}
},
computed: {
rowsToDisplay() {
return Object.keys(this.bitcoinInfo)
.map(key => {
return {
currency: key,
...this.bitcoinInfo[key]
}
}).map((item) => ({
...item,
thirtyDaysDiff: this.calculateDifference(item['7d'], item['30d']),
thirtyDaysDiffClass: this.getDiffClass(item['7d'], item['30d']),
sevenDaysDiff: this.calculateDifference(item['24h'], item['7d']),
sevenDaysDiffClass: this.getDiffClass(item['24h'], item['7d']),
}))
}
},
mounted() {
this.getBitcoinData()
}
}
</script>
have you tried to import your icons inside the template area with <i ...></i>?
here is an working example. Check out the cdn.fontawesome/help-page to get more information.
Vue.createApp({
data () {
return {
isPositive: false,
isNegative: true
}
}
}).mount('#demo')
.negative {
color: red;
}
.positive {
color: green;
}
.neutral {
color: #666;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#next"></script>
<div id="demo">
<i class="fas" :class="isPositive ? 'fa-angle-up positive' : isNegative ? 'fa-angle-down negative' : 'fa-minus neutral' "></i>
<br>
<br>
<button #click="isPositive = !isPositive; isNegative = !isNegative" v-text="'change pos and neg'" />
</div>
so basically you'll bind the icon classes to your own conditions. You could write the conditions for example with the tenary operator into your template area. Hope you get the idea.
Host Fontawesome yourself by following the steps in this Fontawesome documentation.
https://fontawesome.com/docs/web/setup/host-yourself/webfonts
i hope this help.

Inline style with Media query in Gutenberg block

I have a custom Gutenberg block which has a media uploader in the editor and renders a div with a background image on the front end. I want to use the full image as background on desktop and the thumbnail as background on mobile. Is it possible to use the useMediaQuery hook to achieve this? Any guidance will be greatly appreciated.
Below is my code:
const { __ } = wp.i18n;
const { registerBlockType } = wp.blocks;
const { MediaUploadCheck, MediaUpload } = wp.blockEditor;
const { useMediaQuery } = wp.compose;
registerBlockType( 'blockset/test', {
title: __( 'test' ),
attributes: {
imgUrl: { type: 'string', default: '' },
imgMoUrl: { type: 'string', default: '' },
},
edit: (props) {
return (
<div className="content">
<span>Choose a Hero Image</span>
<MediaUploadCheck>
<MediaUpload
onSelect={ ( img ) => {
props.setAttributes( {
imgUrl: img.url,
imgMoUrl: img.sizes.thumbnail.url : '',
} );
} }
render={ ( { open } ) => {
return props.attributes.imgUrl !== ''? (
<div className={ 'hero__preview' }>
<figure className={ 'image' }>
<img
src={ props.attributes.imgUrl }
/>
</figure>
<Button
className={ 'hero__button' }
onClick={ open }
>
Select a New Image
</Button>
</div>
) : (
<div className={ 'hero__container' }>
<p className={ 'hero__description' }>
Pick a hero image from the media library.
</p>
<Button
className={ 'hero__button' }
onClick={ open }
>
Open Media Library
</Button>
</div>
);
} }
/>
</MediaUploadCheck>
</div>
);
},
save( props ) {
const isMobile = useMediaQuery( 'max-width:767px' )
const imgURL = isMobile ? props.attributes.imgMoUrl : props.attributes.imgUrl
return (
<div
className="background-image"
style={ { backgroundImage: `url(${ imgURL })` } }
></div>
);
},
} );
Solved this issue by using the tag.
<style dangerouslySetInnerHTML={ { __html: `
.background-image {background-image: url(${ props.attributes.imgUrl });}
#media (max-width: 767px) {
.background-image {background-image: url(${ props.attributes.imgMoUrl });}
}
` } }></style>

Save function not re-rendering while creating block

Save function while creating a block is not re-rendering in the front end. I made a component for the save which should re-render on state change but it is not. Edit function is working fine for admin.
Basically the setState function is not working for me.
I tried to enqueue the style but it also didn't worked for me.
My Save.js :
const { Component } = wp.element;
import './MyCss.css';
const { Icon } = wp.components;
import unsplash from './unsplash';
export class Save extends React.Component {
constructor(props) {
super(props)
this.state = {
images: [],
currentIndex: 0,
translateValue: 0,
translateValueSmall: 0
}
}
async componentDidMount(){
const response = await unsplash.get('/search/photos',{
params:{query: "cat"},
});
response.data.results.map(result=>{
this.setState(prevState => ({
images: [...prevState.images, result.urls.thumb]
}))
});
}
goToPrevSlide(){
console.log("previous slide");
if(this.state.currentIndex === 0)
return;
this.setState(prevState => ({
currentIndex: prevState.currentIndex - 1,
translateValue: prevState.translateValue + this.slideWidth(),
translateValueSmall: prevState.translateValueSmall + this.slideWidthSmall()/2
}))
}
goToNextSlide(){
if(this.state.currentIndex === this.state.images.length - 1) {
return this.setState({
currentIndex: 0,
translateValue: 0,
translateValueSmall: 0
})
}
this.setState(prevState => ({
currentIndex: prevState.currentIndex + 1,
translateValue: prevState.translateValue + -(this.slideWidth()),
translateValueSmall: prevState.translateValueSmall + -(this.slideWidthSmall())/2
}));
}
slideWidth(){
return document.querySelector('.slide').clientWidth
}
slideWidthSmall(){
return document.querySelector('.sliders').clientWidth
}
render() {
return (
<div className="box" onClick={()=>this.goToPrevSlide()}>
<div className="slider">
<div className="slider-wrapper"
style={{
transform: `translateX(${this.state.translateValue}px)`,
transition: 'transform ease-out 0.95s'
}}>
{
this.state.images.map((image, i) => (
<Slide key={i} image={image} />
))
}
</div>
</div>
<div className="sliders">
<LeftArrow
goToPrevSlide={()=>this.goToPrevSlide()}
/>
<RightArrow
goToNextSlide={()=>this.goToNextSlide()}
/>
<div className="chaitali"
style={{
transform: `translateX(${this.state.translateValueSmall}px)`,
transition: 'transform ease-out 0.95s'
}}>
{
this.state.images.map((image, i) => (
<Slide key={i} image={image} />
))
}
</div>
</div>
</div>
);
}
}
const Slide = ({ image }) => {
const styles = {
backgroundImage: `url(${image})`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
backgroundPosition: '50% 60%'
}
return <div className="slide" style={styles}></div>
}
const LeftArrow = (props) => {
return (
<div onClick={props.goToPrevSlide}>
<Icon className="back arrow" icon="arrow-left"/>
</div>
);
}
const RightArrow = (props) => {
return (
<div onClick={props.goToNextSlide}>
<Icon className="next arrow" icon="arrow-right"/>
</div>
);
}

WordPress Gutenberg Register Multiple Custom Blocks

I am trying to create several custom blocks within Gutenberg. It is only allowing me to register one at a time.
I have tried combining recipe_card_block() and first_block() but that doesn't help.
Both blocks work correctly individually. If I remove recipe_card_block(), first_block() will appear in the inserter (and vice versa). However, when both are present only the first one will show up. Changing the order in which they are registered affects which one appears.
It seems to me they are somehow overwriting each other, but I don't see how that's happening.
Here is the code in functions.php.
function recipe_card_block(){
wp_register_script(
'recipe-card-script', // name of script
get_template_directory_uri() . '/js/recipe-card.js', // path to script
array( 'wp-blocks', 'wp-element', 'wp-editor', 'wp-components' ) // dependencies
);
wp_register_style(
'recipe-card-style',
get_template_directory_uri() . '/css/recipe-card-style.css',
array( 'wp-edit-blocks' )
);
register_block_type('gadam/recipe-card', array(
'editor_script' => 'recipe-card-script', // default script / script to define behavior within the editor
'style' => 'recipe-card-style' // default styles
) );
}
add_action( 'init', 'recipe_card_block' );
function first_block(){
wp_register_script(
'first-block-script', // name of script
get_template_directory_uri() . '/js/first-block.js', // path to script
array( 'wp-blocks', 'wp-element', 'wp-editor' ) // dependencies
);
// styles for editor view
wp_register_style(
'first-block-editor-style',
get_template_directory_uri() . '/css/first-block-editor-style.css',
array( 'wp-edit-blocks' )
);
// default styles
wp_register_style(
'first-block-style',
get_template_directory_uri() . '/css/first-block-style.css',
array( 'wp-edit-blocks' )
);
register_block_type('gadam/first-block', array(
'editor_script' => 'first-block-script', // default script / script to define behavior within the editor
'editor_style' => 'first-block-editor-style', // styles for editor view
'style' => 'first-block-style' // default styles
) );
}
add_action( 'init', 'first_block' );
This is the code in first-block.js
const { registerBlockType } = wp.blocks;
const { RichText, BlockControls, InspectorControls, AlignmentToolbar, FontSizePicker } = wp.editor;
const { Fragment } = wp.element;
registerBlockType( 'gadam/first-block', {
title: 'First Block',
icon: 'welcome-learn-more',
category: 'custom-blocks',
attributes: {
content: {
type: 'string',
source: 'html',
selector: 'p'
},
alignment: {
type: 'string'
},
fontSize: {
type: 'number',
default: 18
}
},
edit( { attributes, className, setAttributes } ) {
const { content, alignment, fontSize } = attributes;
const fontSizes = [
{
name: 'Small',
slug: 'small',
size: 14
},
{
name: 'Normal',
slug: 'normal',
size: 18
},
{
name: 'Large',
slug: 'large',
size: 24
}
];
function onChangeContent( newContent ) {
setAttributes( { content: newContent } );
}
function onChangeAlignment( newAlignment ) {
setAttributes( { alignment: newAlignment } );
}
function onChangeFontSize( newFontSize ) {
setAttributes( { fontSize: newFontSize } );
}
return (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
</BlockControls>
<InspectorControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
<FontSizePicker
fontSizes={ fontSizes }
value={ fontSize }
onChange={ onChangeFontSize }
/>
</InspectorControls>
<RichText
key="editable"
tagName="p"
className={ className }
style={ { textAlign: alignment, fontSize: fontSize } }
onChange={ onChangeContent }
value={ content }
/>
</Fragment>
);
},
save( { attributes } ) {
const { content, alignment, fontSize } = attributes;
const baseClass = 'wp-block-gadam-first-block';
return (
<div class="container">
<div class={ baseClass }>
<RichText.Content
style={ { textAlign: alignment, fontSize: fontSize } }
value={ content }
tagName="p"
/>
</div>
</div>
);
},
} );
And finally, this is recipe-card.js
const { registerBlockType } = wp.blocks;
const { RichText, BlockControls, InspectorControls, AlignmentToolbar, FontSizePicker } = wp.editor;
const { Fragment } = wp.element;
registerBlockType( 'gadam/recipe-card', {
title: 'Recipe Card',
icon: 'index-card',
category: 'custom-blocks',
attributes: {
content: {
type: 'string',
source: 'html',
selector: 'p'
},
alignment: {
type: 'string'
},
fontSize: {
type: 'number',
default: 18
}
},
edit( { attributes, className, setAttributes } ) {
const { content, alignment, fontSize } = attributes;
const fontSizes = [
{
name: 'Small',
slug: 'small',
size: 14
},
{
name: 'Normal',
slug: 'normal',
size: 18
},
{
name: 'Large',
slug: 'large',
size: 24
}
];
function onChangeContent( newContent ) {
setAttributes( { content: newContent } );
}
function onChangeAlignment( newAlignment ) {
setAttributes( { alignment: newAlignment } );
}
function onChangeFontSize( newFontSize ) {
setAttributes( { fontSize: newFontSize } );
}
return (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
</BlockControls>
<InspectorControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
<FontSizePicker
fontSizes={ fontSizes }
value={ fontSize }
onChange={ onChangeFontSize }
/>
</InspectorControls>
<RichText
key="editable"
tagName="p"
className={ className }
style={ { textAlign: alignment, fontSize: fontSize } }
onChange={ onChangeContent }
value={ content }
/>
</Fragment>
);
},
save( { attributes } ) {
const { content, alignment, fontSize } = attributes;
const baseClass = 'wp-block-gadam-recipe-card';
return (
<div class="container">
<div class={ baseClass }>
<RichText.Content
style={ { textAlign: alignment, fontSize: fontSize } }
value={ content }
tagName="p"
/>
</div>
</div>
);
},
} );
For anyone who may come across this in the future:
Look at the top of the two js files I posted. The constants declared in one file are shared by all subsequently registered blocks. So what's happening is when I register first-block the constants are defined. When I register recipe-card it tries to define the constants at the top of the file, but they were already defined by first-block.
The code for recipe-card.js should remove the constants that are already defined by first-block.
registerBlockType( 'gadam/recipe-card', {
title: 'Recipe Card',
icon: 'index-card',
category: 'custom-blocks',
attributes: {
content: {
type: 'string',
source: 'html',
selector: 'p'
},
alignment: {
type: 'string'
},
fontSize: {
type: 'number',
default: 18
}
},
edit( { attributes, className, setAttributes } ) {
const { content, alignment, fontSize } = attributes;
const fontSizes = [
{
name: 'Small',
slug: 'small',
size: 14
},
{
name: 'Normal',
slug: 'normal',
size: 18
},
{
name: 'Large',
slug: 'large',
size: 24
}
];
function onChangeContent( newContent ) {
setAttributes( { content: newContent } );
}
function onChangeAlignment( newAlignment ) {
setAttributes( { alignment: newAlignment } );
}
function onChangeFontSize( newFontSize ) {
setAttributes( { fontSize: newFontSize } );
}
return (
<Fragment>
<BlockControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
</BlockControls>
<InspectorControls>
<AlignmentToolbar
value={ alignment }
onChange={ onChangeAlignment }
/>
<FontSizePicker
fontSizes={ fontSizes }
value={ fontSize }
onChange={ onChangeFontSize }
/>
</InspectorControls>
<RichText
key="editable"
tagName="p"
className={ className }
style={ { textAlign: alignment, fontSize: fontSize } }
onChange={ onChangeContent }
value={ content }
/>
</Fragment>
);
},
save( { attributes } ) {
const { content, alignment, fontSize } = attributes;
const baseClass = 'wp-block-gadam-recipe-card';
return (
<div class="container">
<div class={ baseClass }>
<RichText.Content
style={ { textAlign: alignment, fontSize: fontSize } }
value={ content }
tagName="p"
/>
</div>
</div>
);
},
} );

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