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-block
Code 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.js
Code 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.
- 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 usingupload
. - 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.
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.
Clicking on “Upload Avatar” will display a modal with the suggested dimensions for our crop.
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:
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)
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!
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.