Home / Block and Plugin Tutorials / How to Add a Media Upload Component to Your WordPress Block That Supports Cropping

How to Add a Media Upload Component to Your WordPress Block That Supports Cropping

Global Cloud Computing with Neon Blue Connections. generative AI image

Occasionally, you’ll need to add a media component to your WordPress block, and I’m here to show you how. I’ll demonstrate two ways, one using default WordPress components, and one using a hook that supports cropping.

We’ll be creating a simple avatar upload block, so let’s get started.

Generating the block plugin

We’ll be using @wordpress/create-block to create the block. Navigate to your wp-content folder in Terminal and add this command to generate the block.

npx @wordpress/create-block@latest --namespace=dlxplugins --title="DLX Avatar Block" --wp-scripts --variant=dynamic dlx-avatar-sample-blockCode language: Bash (bash)

The result is a folder structure for our block:

.
└── dlx-avatar-sample-block/
    ├── dlx-avatar-sample-block.php
    └── src/
        ├── block.json
        ├── edit.js
        ├── editor.scss
        ├── index.js
        ├── render.php
        ├── style.scss
        └── view.jsCode language: AsciiDoc (asciidoc)

If you open up edit.js, this is where the bulk of our logic will go. The edit.js file currently looks like this:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
export default function Edit() {
	return (
		<p { ...useBlockProps() }>
			{ __(
				'DLX Avatar Block – hello from the editor!',
				'dlx-avatar-sample-block'
			) }
		</p>
	);
}Code language: JavaScript (javascript)

Let’s quickly add a few attributes to the block.json file to hold the avatar data.

{
	"$schema": "https://schemas.wp.org/trunk/block.json",
	"apiVersion": 3,
	"name": "dlxplugins/dlx-avatar-sample-block",
	"version": "0.1.0",
	"title": "DLX Avatar Block",
	"category": "widgets",
	"icon": "smiley",
	"description": "Example block scaffolded with Create Block tool.",
	"attributes": {
		"avatarId": {
			"type": "number",
			"default": 0
		},
		"avatarUrl": {
			"type": "string",
			"default": ""
		}
	},
	"example": {},
	"supports": {
		"html": false
	},
	"textdomain": "dlx-avatar-sample-block",
	"editorScript": "file:./index.js",
	"editorStyle": "file:./index.css",
	"style": "file:./style-index.css",
	"render": "file:./render.php",
	"viewScript": "file:./view.js"
}

Code language: JSON / JSON with Comments (json)

Let’s modify edit.js to accept these new attributes:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<p { ...useBlockProps() }>
			{ __(
				'DLX Avatar Block – hello from the editor!',
				'dlx-avatar-sample-block'
			) }
		</p>
	);
}Code language: JavaScript (javascript)

I’ve added a props argument, and extracted out the attributes.

Let’s dive into the first technique, which uses the MediaUpload component.

Technique One: using the MediaUploadCheck and MediaUpload components

For this first example, we’ll use the MediaUploadCheck and MediaUpload components. Let’s add those to the top of the imports.

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { MediaUploadCheck, MediaUpload } from '@wordpress/block-editor';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<p { ...useBlockProps() }>
			{ __(
				'DLX Avatar Block – hello from the editor!',
				'dlx-avatar-sample-block'
			) }
		</p>
	);
}

Code language: JavaScript (javascript)

The MediaUploadCheck component is just a permissions wrapper, which ensures that the user has image-uploading permissions. The bulk of the logic is in the MediaUpload component.

Next, we’ll need to add a Button component. This will trigger the media library to open.

mport { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { MediaUploadCheck, MediaUpload } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';
Code language: JavaScript (javascript)

Let’s replace the existing return statement to return our media upload interface using the MediaUpload component.

export default function Edit( props ) {
	const { attributes, setAttributes } = props;

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<div { ...useBlockProps() }>
			<MediaUploadCheck>
				<MediaUpload
					onSelect={ ( media ) => {
						/* do stuff here */
					} }
					title={ __( 'Select an Avatar', 'dlx-avatar-sample-block' ) }
					mode={ 'upload' }
					multiple={ false }
					allowedTypes={ [ 'image' ] }
					value={ avatarId }
					render={ ( { open } ) => (
						<Button
							variant="secondary"
							onClick={ () => {
								open();
							} }
						>
							{ __( 'Upload Avatar', 'dlx-avatar-sample-block' ) }
						</Button>
					) }
				/>
			</MediaUploadCheck>
		</div>
	);
}Code language: JavaScript (javascript)

As mentioned previously, the bulk of the logic is in the MediaUpload component. Let’s go over the attributes used in the component.

Sample Block with Image Upload Button
Sample Block with Image Upload Button
  • onSelect: This callback allows us to save the media as attributes for retrieval later. We’ll fill this out in a bit.
  • title: This is the title of the modal.
  • mode: This can be browse, upload, and some other items. For this, we’re using upload.
  • multiple: Since we’re accepting a single image here, we’re setting this to false.
  • allowedTypes: This allows you to set which image types or upload types are accepted. In this case, it’s just images. This can be any valid mime type.
  • value: The media value of the image selected.
  • render: What to render for the upload interface. In our case, it’ll be a button and image (when available).

Let’s fill out the onSelect callback so that our media saves when selected.

onSelect={ ( media ) => {
	setAttributes( {
		avatarId: media.id,
		avatarUrl: media.url,
	} );
} }Code language: JavaScript (javascript)

We’re saving the ID and URL to our attributes. Once that’s done, we can display the image in the render callback.

render={ ( { open } ) => (
	<>
		<Button
			variant="secondary"
			onClick={ () => {
				open();
			} }
		>
			{ __( 'Upload Avatar', 'dlx-avatar-sample-block' ) }
		</Button>
		{
			avatarUrl && (
				<img
					src={ avatarUrl }
					alt={ __( 'Avatar', 'dlx-avatar-sample-block' ) }
					style={{
						display: 'block',
						maxWidth: '250px',
						height: 'auto',
					}}
				/>
			)
		}
	</>
) }Code language: JavaScript (javascript)

If there’s an avatarUrl, we output an image.

Image and Add Avatar Button
Image and Add Avatar Button

Here’s the full code for the media upload component:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { MediaUploadCheck, MediaUpload } from '@wordpress/block-editor';
import { Button } from '@wordpress/components';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<div { ...useBlockProps() }>
			<MediaUploadCheck>
				<MediaUpload
					onSelect={ ( media ) => {
						setAttributes( {
							avatarId: media.id,
							avatarUrl: media.url,
						} );
					} }
					title={ __( 'Select an Avatar', 'dlx-avatar-sample-block' ) }
					mode={ 'upload' }
					multiple={ false }
					allowedTypes={ [ 'image' ] }
					value={ avatarId }
					render={ ( { open } ) => (
						<>
							<Button
								variant="secondary"
								onClick={ () => {
									open();
								} }
							>
								{ __( 'Upload Avatar', 'dlx-avatar-sample-block' ) }
							</Button>
							{
								avatarUrl && (
									<img
										src={ avatarUrl }
										alt={ __( 'Avatar', 'dlx-avatar-sample-block' ) }
										style={{
											display: 'block',
											maxWidth: '250px',
											height: 'auto',
										}}
									/>
								)
							}
						</>
					) }
				/>
			</MediaUploadCheck>
		</div>
	);
}
Code language: JavaScript (javascript)

For the frontend, we’ll modify render.php to have the following:

<?php
$avatar_id = absint( $attributes['avatarId'] );

// Output the image.
if ( $avatar_id ) {
	echo wp_get_attachment_image( $avatar_id, 'full' );
}Code language: PHP (php)

We now have a functional avatar block, but there’s one thing lacking: cropping. I’ll demonstrate cropping in the following example, using a hook for the media upload.

Technique Two: using a custom wp.media hook with crop support

For this next technique, we’ll use the built-in media library that is loaded in the block editor, accessing its properties directly.

For this, we’ll need a hook to help us launch and manage the media dialogue.

Here’s the full code for the hook:

import { __ } from '@wordpress/i18n';
const getCropSettings = ( overrides = {} ) => {
	// Set the settings for the media uploader and cropper.
	let settings = {
		id: '',
		attachmentId: 0,
		aspectRatio: '1:1',
		suggestedWidth: '500',
		suggestedHeight: '500',
		nonce: '',
		postId: 0,
		title: __( 'Image', 'wp-plugin-info-card' ),
		buttonLabel: __( 'Add Image', 'wp-plugin-info-card' ),
		main: this,
	};
	settings = { ...settings, ...overrides };
	return settings;
};

const getCropControl = ( overrides = {} ) => {
	const settings = getCropSettings( overrides );
	const cropControl = {
		id: 'control-id',
		params: {
			flex_width: false, // set to true if the width of the cropped image can be different to the width defined here
			flex_height: false, // set to true if the height of the cropped image can be different to the height defined here
			width: settings.suggestedWidth, // set the desired width of the destination image here
			height: settings.suggestedHeight, // set the desired height of the destination image here
		},
	};
	return cropControl;
};

const useMediaUploader = ( props ) => {
	/**
	 * Retrieve crop options for an attachment.
	 *
	 * @param {Object} attachment   Attachment image object.
	 * @param {Object} controller   Media controller object.
	 * @param {Object} cropSettings Crop settings.
	 *
	 * @return {Object} Cropping options.
	 */
	const cropOptions = ( attachment, controller, cropSettings ) => {
		const settings = getCropSettings( cropSettings );
		const control = controller.get( 'control' );
		const realWidth = attachment.get( 'width' );
		const realHeight = attachment.get( 'height' );

		let xInit = parseInt( control.params.width, 10 );
		let yInit = parseInt( control.params.height, 10 );

		const ratio = xInit / yInit;
		const ratioReal = realWidth / realHeight;

		// Determine if user can skip crop.
		let canSkipCrop = false;

		// If ratios match, can skip crop.
		if ( ratio === ratioReal ) {
			canSkipCrop = true;
		}
		controller.set( 'canSkipCrop', canSkipCrop );

		let xImg = xInit;
		let yImg = yInit;

		if ( realWidth / realHeight > ratio ) {
			if ( yImg > realHeight ) {
				yImg = realHeight;
			}
			yInit = yImg;
			xInit = yInit * ratio;
		} else {
			if ( xImg > realWidth ) {
				xImg = realWidth;
			}
			xInit = xImg;
			yInit = xInit / ratio;
		}

		let x1 = ( realWidth - xInit ) / 2;
		let y1 = ( realHeight - yInit ) / 2;

		if ( x1 === 0 ) {
			if ( ratio > 0 ) {
				x1 = y1 * ratio;
			} else {
				x1 = y1 / ratio;
			}
		}
		if ( y1 === 0 ) {
			if ( ratio > 0 ) {
				y1 = x1 * ratio;
			} else {
				y1 = x1 / ratio;
			}
		}

		let cropWidthX2 = 0;
		let cropHeightY2 = 0;
		if ( xInit + x1 > realWidth ) {
			cropWidthX2 = xInit - 1;
		} else {
			cropWidthX2 = xInit + x1;
		}
		if ( yInit + y1 > realHeight ) {
			cropHeightY2 = yInit - 1;
		} else {
			cropHeightY2 = yInit + y1;
		}

		const imgSelectOptions = {
			handles: true,
			keys: true,
			instance: true,
			persistent: true,
			imageWidth: realWidth,
			imageHeight: realHeight,
			x1,
			y1,
			x2: cropWidthX2,
			y2: cropHeightY2,
			aspectRatio: settings.aspectRatio,
		};
		return imgSelectOptions;
	};
	return {
		openMediaUploader: ( cropSettings, callback ) => {
			const settings = getCropSettings( cropSettings );
			const cropControl = getCropControl( cropSettings );
			const uploader = wp.media( {
				states: [
					new wp.media.controller.Library( {
						title: cropSettings.title || settings.title,
						library: wp.media.query( { type: 'image' } ),
						multiple: false,
						date: false,
						priority: 20,
						suggestedWidth: settings.suggestedWidth,
						suggestedHeight: settings.suggestedHeight,
					} ),
					new wp.media.controller.CustomizeImageCropper( {
						control: cropControl,
						imgSelectOptions: ( attachment, controller ) => cropOptions( attachment, controller, cropSettings ),
					} ),
				],
			} );

			// Set the toolbar.
			uploader.on(
				'toolbar:create',
				function( toolbar ) {
					const options = {};
					options.items = {};
					options.items.select = {
						text: settings.buttonLabel,
						style: 'primary',
						click: wp.media.view.Toolbar.Select.prototype.clickSelect,
						requires: { selection: true },
						event: 'select',
						reset: false,
						close: false,
						state: false,
						syncSelection: true,
					};
					this.createSelectToolbar( toolbar, options );
				},
				uploader,
			);

			//For when the Add Profile Image is clicked
			let originalAttachmentId = 0;
			uploader.on( 'select', function() {
				// Get avatar attributes.
				const attachment = uploader.state().get( 'selection' ).first().toJSON();

				// Get original attachment ID.
				originalAttachmentId = attachment.id;

				// Calculate ratio.
				const ratio = attachment.width / attachment.height;
				const desiredRatio = cropControl.params.width / cropControl.params.height;
				if ( ratio === desiredRatio ) {
					const selection = uploader.state().get( 'selection' ).single();
					callback( selection.attributes );
					uploader.close();
				} else {
					uploader.setState( 'cropper' );
				}
			} );
			//When the remove buttons is clicked
			uploader.on( 'remove', function() {
			} );

			//For when the window is closed (update the thumbnail)
			uploader.on( 'escape', function() {
			} );

			// When image is cropped.
			uploader.on( 'cropped', function( croppedImage ) {
				callback( croppedImage );
			} );

			// When image cropping is skipped.
			uploader.on( 'skippedcrop', function( selection ) {
				callback( selection.attributes );
			} );

			uploader.on( 'open', function() {
				const attachment = wp.media.attachment( settings.attachmentId );
				const selection = uploader.state( 'library' ).get( 'selection' );
				selection.add( attachment );
			} );
			uploader.open();
		},
	};
};
export default useMediaUploader;
Code language: JavaScript (javascript)

Let’s create a new folder in the src folder called hooks and place the useMediaUploader.js file in there.

The folder structure should look like this:

.
└── dlx-avatar-sample-block/
    ├── dlx-avatar-sample-block.php
    └── src/
        ├── block.json
        ├── edit.js
        ├── editor.scss
        ├── index.js
        ├── render.php
        ├── style.scss
        ├── view.js
        └── hooks/
            └── useMediaUploader.js
Code language: AsciiDoc (asciidoc)

We’ll add the hook to the top of our imports. The code should look like this for the block:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { Button } from '@wordpress/components';
import useMediaUploader from './hooks/useMediaUploader';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<div { ...useBlockProps() }>
			placeholder
		</div>
	);
}Code language: JavaScript (javascript)

The useMediaUploader hook returns a function called openMediaUploader, which we can destructure.

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { Button } from '@wordpress/components';
import useMediaUploader from './hooks/useMediaUploader';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;
	const { openMediaUploader } = useMediaUploader();

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<div { ...useBlockProps() }>
			placeholder
		</div>
	);
}

Code language: JavaScript (javascript)

From there, we can create a button to initialize the hook.

return (
	<div { ...useBlockProps() }>
		<Button
			variant="secondary"
			onClick={ () => {
				openMediaUploader( {
					attachmentId: avatarId,
					title: __( 'Select an Avatar Image', 'dlx-avatar-sample-block' ),
					buttonLabel: __( 'Select Avatar', 'dlx-avatar-sample-block' ),
					suggestedWidth: 500,
					suggestedHeight: 500,
					aspectRatio: '1:1',
				}, ( media ) => {
					setAttributes( {
						avatarId: media.id,
						avatarUrl: media.url,
					} );
				} );
			} }
		>
			{ __( 'Upload Avatar', 'dlx-avatar' ) }
		</Button>
	</div>
);Code language: JavaScript (javascript)

The openMediaUploader takes in two arguments: modal/crop parameters and a callback.

You’ll notice that I’m passing several properties:

  • attachmentId: This is the ID of the attachment so the right image is selected in the media dialogue.
  • title: This is the modal title.
  • buttonLabel: This is the label of the button in the media uploader.
  • suggestedWidth: The crop width in pixels.
  • suggestedHeight: The crop height in pixels.
  • aspectRatio: This is the aspect ratio of the crop.

The callback, if successful, returns a media object, which I assign as attributes.

We now have a functional upload button.

MediaUpload Button for Hook Media Uploader
MediaUpload Button for Hook Media Uploader

Clicking on “Upload Avatar” will display a modal with the suggested dimensions for our crop.

Suggested Dimensions on the Media Upload Dialogue
Suggested Dimensions on the Media Upload Dialogue

If the dimensions don’t match, or the aspect ratio is off, then you will be prompted to crop an image.

Finally, we need to output the image to the block editor:

return (
	<div { ...useBlockProps() }>
		<Button
			variant="secondary"
			onClick={ () => {
				openMediaUploader( {
					attachmentId: avatarId,
					title: __( 'Select an Avatar Image', 'dlx-avatar-sample-block' ),
					buttonLabel: __( 'Select Avatar', 'dlx-avatar-sample-block' ),
					suggestedWidth: 500,
					suggestedHeight: 500,
					aspectRatio: '1:1',
				}, ( media ) => {
					setAttributes( {
						avatarId: media.id,
						avatarUrl: media.url,
					} );
				} );
			} }
		>
			{ __( 'Upload Avatar', 'dlx-avatar' ) }
		</Button>
		{ avatarUrl && (
			<img
				src={ avatarUrl }
				alt={ __( 'Avatar Image', 'dlx-avatar-sample-block' ) }
				style={ {
					display: 'block',
					maxWidth: '175px',
					height: 'auto',
				} }
			/>
		) }
	</div>
);
Code language: JavaScript (javascript)

And this is what it’ll look like:

DLX Cropped Image Showing in Block Editor
DLX Cropped Image Showing in Block Editor

Here’s the full code for edit.js:

import { __ } from '@wordpress/i18n';
import { useBlockProps } from '@wordpress/block-editor';
import './editor.scss';
import { Button } from '@wordpress/components';
import useMediaUploader from './hooks/useMediaUploader';

export default function Edit( props ) {
	const { attributes, setAttributes } = props;
	const { openMediaUploader } = useMediaUploader();

	const {
		avatarId,
		avatarUrl,
	} = attributes;
	return (
		<div { ...useBlockProps() }>
			<Button
				variant="secondary"
				onClick={ () => {
					openMediaUploader( {
						attachmentId: avatarId,
						title: __( 'Select an Avatar Image', 'dlx-avatar-sample-block' ),
						buttonLabel: __( 'Select Avatar', 'dlx-avatar-sample-block' ),
						suggestedWidth: 500,
						suggestedHeight: 500,
						aspectRatio: '1:1',
					}, ( media ) => {
						setAttributes( {
							avatarId: media.id,
							avatarUrl: media.url,
						} );
					} );
				} }
			>
				{ __( 'Upload Avatar', 'dlx-avatar' ) }
			</Button>
			{ avatarUrl && (
				<img
					src={ avatarUrl }
					alt={ __( 'Avatar Image', 'dlx-avatar-sample-block' ) }
					style={ {
						display: 'block',
						maxWidth: '175px',
						height: 'auto',
					} }
				/>
			) }
		</div>
	);
}Code language: JavaScript (javascript)

Finally, render.php is updated to show the block on the frontend:

<?php
$avatar_id = absint( $attributes['avatarId'] );

// Output the image.
if ( $avatar_id ) {
	echo wp_get_attachment_image( $avatar_id, 'full' );
}Code language: PHP (php)
Sample Block Output of an Avatar Block
Sample Block Output of an Avatar Block

Conclusion

In this tutorial, I demonstrated two ways to add a media upload component to your block: via built-in WP components, and a media uploader hook.

If you need more control over the cropping and appearance of the resulting image, then a hook is the way to go. Otherwise, you’re safe using standard components.

If you have any questions, please leave a comment or @ me on Twitter (@dlxplugins). Thanks for reading.

Like this tutorial? There's more like it. Subscribe today!

Name(Required)

Ronald Huereca
By: Ronald Huereca
Published On: on July 18, 2024

Ronald Huereca founded DLX Plugins in 2022 with the goal of providing deluxe plugins available for download. Find out more about DLX Plugins, check out some tutorials, and check out our plugins.

Leave Your Valuable Feedback

Your email address will not be published. Required fields are marked *

Shopping Cart
  • Your cart is empty.
Scroll to Top