Admin forms in Magento 2 are built with UI Components. There are different types of UI Components that allow us to create many types of fields in forms. One of them is the ImageUploader component.

In this tutorial, we'll see how to add an ImageUploader component to a form on the admin side that allows us to upload images.

Prerequisites

In this section we'll go over the basics of creating a module, adding a menu item on the admin side, and creating a grid that shows the images we uploaded before we start working on our form. If you already know how to do all of that or you just need to see how to create a form with the ImageUploader component, you can skip this section.

Create the Module

We'll quickly go over how to create a module in Magento 2. Create in app/code the directories VENDOR/MODULE where VENDOR is your name or your company's name and MODULE is the name of the module. We'll name this module ImageUploader.

Make sure to replace all instances of VENDOR with the vendor name you choose, as it will be in every code snippet throughout the tutorial.

Then, inside ImageUploader create registration.php with the following content:

<?php

use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(ComponentRegistrar::MODULE, 'VENDOR_ImageUploader', __DIR__);

Create a directory called etc and inside it create module.xml with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="VENDOR_ImageUploader" setup_version="0.0.1" />
</config>

These are the only files required to create a module. Next, we'll need to create the scripts to create a table in the database to store the path of the images we'll upload. Create a directory called Setup inside ImageUploader, then create the file InstallSchema.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Setup;

use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\InstallSchemaInterface;
use Magento\Framework\Setup\SchemaSetupInterface;
use Magento\Framework\Setup\ModuleContextInterface;

class InstallSchema implements InstallSchemaInterface {

  public function install(SchemaSetupInterface $setup, ModuleContextInterface $context)
  {
    $setup->startSetup();

    $imagesTableName = $setup->getTable('VENDOR_images');
    if (!$setup->getConnection()->isTableExists($imagesTableName)) {
      $imagesTable = $setup->getConnection()->newTable($imagesTableName)
        ->addColumn(
          'image_id',
          Table::TYPE_INTEGER,
          null,
          [
            Table::OPTION_IDENTITY => true,
            Table::OPTION_PRIMARY => true,
            Table::OPTION_UNSIGNED => true,
            Table::OPTION_NULLABLE => false,
          ],
          'Image Id'
        )
        ->addColumn(
          'path',
          Table::TYPE_TEXT,
          255,
          [
            Table::OPTION_NULLABLE => false
          ],
          'Image Path'
        );

        $setup->getConnection()->createTable($imagesTable);
    }

    $setup->endSetup();
  }
}

Make sure to replace VENDOR in the table name with the name of your vendor.

Now, we'll create the necessary models for our table. First, create the directories Api/Data under ImageUploader, and inside Data create the file ImageInterface.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Api\Data;

interface ImageInterface {
  const ID = 'image_id';
  const PATH = 'path';

  public function getPath ();

  public function setPath ($value);
}

Then, create the directory Model under ImageUploader and inside it create Image.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Model;

use Magento\Framework\DataObject\IdentityInterface;
use Magento\Framework\Model\AbstractModel;
use VENDORE\ImageUploader\Api\Data\ImageInterface;
use VENDORE\ImageUploader\Model\ResourceModel\Image as ResourceModelImage;

class Image extends AbstractModel implements ImageInterface, IdentityInterface {
  const CACHE_TAG = 'VENDOR_images';

  public function getIdentities()
  {
    return [
      self::CACHE_TAG . '_' . $this->getId(),
    ];
  }

  protected function _construct () {
    $this->_init(ResourceModelImage::class);
  }

  public function getPath()
  {
    return $this->getData(self::PATH);
  }

  public function setPath($value)
  {
    return $this->setData(self::PATH, $value);
  }
}

Make sure to replace all instances of VENDOR with your vendor name.

Under the directory Model create the directory ResourceModel. Under ResourceModel create the file Image.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Model\ResourceModel;

use Magento\Framework\Model\ResourceModel\Db\AbstractDb;

class Image extends AbstractDb {
  protected function _construct () {
    return $this->_init('VENDOR_images', 'image_id');
  }
}

Again, make sure to replace VENDOR with your vendor name.

Under the ResourceModel directory create a directory called Image, and under Image create Collection.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Model\ResourceModel\Image;

use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use VENDOR\ImageUploader\Model\Image;
use VENDOR\ImageUploader\Model\ResourceModel\Image as ResourceModelImage;

class Collection extends AbstractCollection {
  protected function _construct()
  {
    $this->_init(Image::class, ResourceModelImage::class);
  }
}

Finally, create the file di.xml with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
  <preference for="VENDOR\ImageUploader\Api\Data\ImageInterface" type="VENDOR\ImageUploader\Api\Data\Image" />
</config>

Now, everything is ready for our models. To enable and compile our module, run the following commands in the root of the Magento project:

php bin/magento setup:upgrade
php bin/magento setup:di:compile

If no errors occur, our module has been created successfully.

Add the Routes

To add routes to be able to access our module on the admin side, create under etc the directory adminhtml, and inside adminhtml create routes.xml with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="imageuploader" frontName="imageuploader">
            <module name="VENDOR_ImageUploader" />
        </route>
    </router>
</config>

Add a Menu Item on The Admin Side

In this section, we'll add a menu item to be able to access the page to upload images. First, create under etc the file acl.xml with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
              <resource id="VENDOR_ImageUploader::upload" title="Upload Images" translate="title" sortOrder="30" />
            </resource>
        </resources>
    </acl>
</config>

Then, inside etc/adminhtml create menu.xml with the following content:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="VENDOR_ImageUploader::images_uploader" title="Image Uploader" translate="title" 
          module="VENDOR_ImageUploader" sortOrder="20" resource="VENDOR_ImageUploader::upload" />
        <add id="VENDOR_ImageUploader::images" title="All Images" translate="title" 
          module="VENDOR_ImageUploader" sortOrder="0" parent="VENDOR_ImageUploader::images_uploader"
          action="imageuploader/images" resource="VENDOR_ImageUploader::upload" />
    </menu>
</config>

This will create a menu item called Image Uploader and when clicking on it the submenu will include the item All Images.

Now, run the following command to compile the changes:

php bin/magento setup:di:compile

Go to the admin side now and login. You'll see in the sidebar a new menu item called Image Uploader.

Create the Image Listing Page

We'll now create the page All Images lead to, which will be a grid of the images uploaded.

Create first the directories Controller\Adminhtml\Images under ImageUploader. Then, create the file Index.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Controller\Adminhtml\Images;

class Index extends \Magento\Backend\App\Action {

  /**
   *
   * @var \Magento\Framework\View\Result\PageFactory
   */
  protected $resultPageFactory;

  public function __construct(
    \Magento\Backend\App\Action\Context $context,
    \Magento\Framework\View\Result\PageFactory $resultPageFactory
  )
  {
    parent::__construct($context);
    $this->resultPageFactory = $resultPageFactory;
  }

  public function execute()
  {
    /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
    $resultPage = $this->resultPageFactory->create();
    $resultPage->setActiveMenu('VENDOR_ImageUploader::images_uploader');
    $resultPage->getConfig()->getTitle()->prepend(__('Images'));
    return $resultPage;
  }
}

Then, under ImageUploader create the directories view/adminhtml/layout and inside layout create imageuploader_images_index.xml with the following content:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
  <body>
    <referenceContainer name="content">
      <uiComponent name="images_list" />
    </referenceContainer>
  </body>
</page>

After that, create under view/adminhtml the directory ui_component, and inside that directory create the file images_list.xml with the following content:

<?xml version="1.0"?>
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">images_list.images_list_data_source</item>
        </item>
    </argument>
    <settings>
        <buttons>
            <button name="upload">
                <url path="*/*/upload"/>
                <class>primary</class>
                <label translate="true">Upload Images</label>
            </button>
        </buttons>
        <spinner>images_columns</spinner>
        <deps>
            <dep>images_list.images_list_data_source</dep>
        </deps>
    </settings>
    <dataSource name="images_list_data_source" component="Magento_Ui/js/grid/provider">
        <settings>
            <storageConfig>
                <param name="indexField" xsi:type="string">image_id</param>
            </storageConfig>
            <updateUrl path="mui/index/render"/>
        </settings>
        <dataProvider class="Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider" name="images_list_data_source">
            <settings>
                <requestFieldName>id</requestFieldName>
                <primaryFieldName>image_id</primaryFieldName>
            </settings>
        </dataProvider>
    </dataSource>
    <columns name="images_columns">
      <column name="image_id" sortOrder="10">
            <settings>
                <filter>text</filter>
                <dataType>text</dataType>
                <label translate="true">ID</label>
            </settings>
        </column>
        <column name="path" component="Magento_Ui/js/grid/columns/thumbnail" class="VENDOR\ImageUploader\Ui\Component\Columns\Thumbnail">
          <settings>
              <hasPreview>0</hasPreview>
              <addField>false</addField>
              <label translate="true">Thumbnail</label>
              <sortable>false</sortable>
          </settings>
        </column>
    </columns>
</listing>

This will create a grid listing that will show the uploaded images. It will show 2 columns, the ID and a thumbnail. To show the thumbnail, we need to create the class for the UI Component. Create under ImageUploader the directories Ui\Component\Columns, and inside Columns create the file Thumbnail.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Ui\Component\Columns;

use Magento\Backend\Model\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Store\Model\StoreManagerInterface;
use Magento\Ui\Component\Listing\Columns\Column;

class Thumbnail extends Column {

  /**
   *
   * @var StoreManagerInterface
   */
  protected $storeManagerInterface;

  public function __construct(
    ContextInterface $context,
    UiComponentFactory $uiComponentFactory,
    StoreManagerInterface $storeManagerInterface,
    array $components = [],
    array $data = []
  ) {
      parent::__construct($context, $uiComponentFactory, $components, $data);
      $this->storeManagerInterface = $storeManagerInterface;
  }

  public function prepareDataSource(array $dataSource)
  {
    foreach($dataSource["data"]["items"] as &$item) {
      if (isset($item['path'])) {
        $url = $this->storeManagerInterface->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $item['path'];
        $item['path_src'] = $url;
        $item['path_alt'] = $item['image_id'];
        $item['path_link'] = $url;
        $item['path_orig_src'] = $url;
      }
    }

    return $dataSource;
  }
}

This class will extend Magento\Ui\Component\Listing\Columns\Column and override the function prepareDataSource. In this function, we're making changes to the path column to show a thumbnail. To be able to show a thumbnail using the component Magento_Ui/js/grid/columns/thumbnail, we need to add to each row in the table the fields FIELD_src, FIELD_alt, FIELD_link and FIELD_orig_src, where FIELD is the name of the field of the thumbnail, in this case, it's path.

Finally, add the following to etc/di.xml:

<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="images_list_data_source" xsi:type="string">
                    VENDOR\ImageUploader\Model\ResourceModel\Image\Grid\Collection
                </item>
            </argument>
        </arguments>
    </type>
    <virtualType name="VENDOR\ImageUploader\Model\ResourceModel\Image\Grid\Collection"
                 type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
        <arguments>
            <argument name="mainTable" xsi:type="string">VENDOR_images</argument>
            <argument name="resourceModel" xsi:type="string">VENDOR\ImageUploader\Model\ResourceModel\Image
            </argument>
        </arguments>
    </virtualType>

Our image listing page is ready. We need to compile the changes first:

php bin/magento setup:di:compile

Then, log in to the admin side again and click on ImageUploader -> All Images. A page with an empty table will show and a button to upload new images.

Now, we're ready to create the form and add the ImageUploader component to it.

Create UI Form

First, we'll need to create the controller that will handle the request. Create under ImageUploader\Controller\Adminhtml\Images the file Upload.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Controller\Adminhtml\Images;

class Upload extends \Magento\Backend\App\Action {

  /**
   *
   * @var \Magento\Framework\View\Result\PageFactory
   */
  protected $resultPageFactory;

  public function __construct(
    \Magento\Backend\App\Action\Context $context,
    \Magento\Framework\View\Result\PageFactory $resultPageFactory
  )
  {
    parent::__construct($context);
    $this->resultPageFactory = $resultPageFactory;
  }

  public function execute()
  {
    /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
    $resultPage = $this->resultPageFactory->create();
    $resultPage->setActiveMenu('VENDOR_ImageUploader::images_uploader');
    $resultPage->getConfig()->getTitle()->prepend(__('Upload Image'));
    return $resultPage;
  }
}

Like Index.php, this one just shows the page. We'll need to create the layout for this page. Create under view/adminhtml/layout the file imageuploader_images_upload.xml with the following content:

<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
  <body>
    <referenceContainer name="content">
      <uiComponent name="images_form" />
    </referenceContainer>
  </body>
</page>

This is just showing a UI Component called images_form, which we'll create next. Create under view/adminhtml/ui_component the file images_form.xml with the following content:

<?xml version="1.0"?>
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">images_form.images_form_data_source</item>
        </item>
        <item name="label" xsi:type="string" translate="true">Upload Images</item>
        <item name="reverseMetadataMerge" xsi:type="boolean">true</item>
        <item name="template" xsi:type="string">templates/form/collapsible</item>
        <item name="config" xsi:type="array">
            <item name="dataScope" xsi:type="string">data</item>
            <item name="namespace" xsi:type="string">images_form</item>
        </item>
    </argument>
    <settings>
        <buttons>
            <button name="save" class="VENDOR\ImageUploader\Block\Adminhtml\Form\UploadButton"/>
            <button name="back" class="VENDOR\ImageUploader\Block\Adminhtml\Form\BackButton"/>
        </buttons>
        <deps>
            <dep>images_form.images_form_data_source</dep>
        </deps>
    </settings>
    <dataSource name="images_form_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="name" xsi:type="string">images_form_data_source</argument>
            <argument name="class" xsi:type="string">VENDOR\ImageUploader\Ui\Component\Form\DataProvider</argument>
            <argument name="primaryFieldName" xsi:type="string">image_id</argument>
            <argument name="requestFieldName" xsi:type="string">id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="submit_url" xsi:type="url" path="*/*/save"/>
                </item>
            </argument>
        </argument>
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/form/provider</item>
            </item>
        </argument>
    </dataSource>
    <fieldset name="image">
      <settings>
          <label translate="true">Upload Images</label>
      </settings>
      <field name="image" formElement="imageUploader">
        <settings>
            <label translate="true">Images</label>
            <componentType>imageUploader</componentType>
            <validation>
                <rule name="required-entry" xsi:type="boolean">true</rule>
            </validation>
        </settings>
        <formElements>
            <imageUploader>
                <settings>
                    <allowedExtensions>jpg jpeg png</allowedExtensions>
                    <maxFileSize>2097152</maxFileSize>
                    <uploaderConfig>
                        <param xsi:type="string" name="url">imageuploader/images/tempUpload</param>
                    </uploaderConfig>
                </settings>
            </imageUploader>
        </formElements>
    </field>
  </fieldset>
</form>

This just creates the UI Form component. Before we dive into the components and classes this form needs, let's look at the ImageUploader component:

<field name="image" formElement="imageUploader">
    <settings>
        <label translate="true">Images</label>
        <componentType>imageUploader</componentType>
        <validation>
            <rule name="required-entry" xsi:type="boolean">true</rule>
        </validation>
    </settings>
    <formElements>
        <imageUploader>
            <settings>
                <allowedExtensions>jpg jpeg png</allowedExtensions>
                <maxFileSize>2097152</maxFileSize>
                <uploaderConfig>
                    <param xsi:type="string" name="url">imageuploader/images/tempUpload</param>
                </uploaderConfig>
            </settings>
        </imageUploader>
    </formElements>
</field>

Notice that we've defined this field as an ImageUploader through <componentType>imageUploader</componentType>. We've also made this field required by adding <rule name="required-entry" xsi:type="boolean">true</rule> to <validation>. Then, inside <formElements>, we're adding a couple of settings for the <imageUploader>. allowedExtensions allows us to specify the extensions allowed, which are in this case jpg, jpeg, and png. maxFileSize is the maximum size allowed for the file being uploaded. and finally, the param url inside uploaderConfig is the url to send to the uploaded image for temporary upload. This is used to show a preview of the image being uploaded.

This form component needs a few PHP Classes to be created to be able to function correctly.

First, we've added 2 buttons, back and upload:

<button name="save" class="VENDOR\ImageUploader\Block\Adminhtml\Form\UploadButton"/>
            <button name="back" class="VENDOR\ImageUploader\Block\Adminhtml\Form\BackButton"/>

So, let's create the classes for them. Create first the directories Block\Adminhtml\Form. Then, create under Form the file UploadButton.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Block\Adminhtml\Form;

use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class UploadButton implements ButtonProviderInterface {
  public function getButtonData()
  {
    return [
      'label' => __('Upload'),
      'class' => 'save primary',
      'data_attribute' => [
          'mage-init' => ['button' => ['event' => 'save']],
          'form-role' => 'save',
      ],
      'sort_order' => 90,
    ];
  }
}

This button just takes the user to the save path. The save path is defined in images_form.xml in this line:

<item name="submit_url" xsi:type="url" path="*/*/save"/>

Also, create the file BackButton.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Block\Adminhtml\Form;

use Magento\Backend\Model\UrlInterface;
use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface;

class BackButton implements ButtonProviderInterface {

  /** @var UrlInterface */
  protected $urlInterface;

  public function __construct(
    UrlInterface $urlInterface
  )
  {
    $this->urlInterface = $urlInterface;
  }
  
    public function getButtonData()
    {
        return [
            'label' => __('Back'),
            'on_click' => sprintf("location.href = '%s';", $this->getBackUrl()),
            'class' => 'back',
            'sort_order' => 10
        ];
    }
    
    public function getBackUrl()
    {
        return $this->urlInterface->getUrl('*/*/');
    }
}

This button just takes the user back to the index page, which is the page that has the image listing.

Next, we need to create the following classes that are defined in images_form.xml:

<argument name="class" xsi:type="string">VENDOR\ImageUploader\Ui\Component\Form\DataProvider</argument>

This is the data provider class. Usually, this class is very important when the form has an edit functionality. This is where the data for the record being edited is filled to show in the form fields.

Create under the directory Ui\Component\Form the file DataProvider.php with the following content:

<?php 

namespace VENDOR\ImageUploader\Ui\Component\Form;

use Magento\Framework\Registry;

class DataProvider extends \Magento\Ui\DataProvider\AbstractDataProvider {
  public function __construct(
    string $name,
    string $primaryFieldName,
    string $requestFieldName,
    Registry $registry,
    \VENDOR\ImageUploader\Model\ResourceModel\Image\CollectionFactory $imageCollectionFactory,
    array $meta = [],
    array $data = []
  )
  {
      parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
      $this->registry = $registry;
      $this->collection = $imageCollectionFactory->create();
  }

  public function getData()
  {
    return [];
  }

}

This just returns an empty array since we don't have the edit functionality in our form.

Finally, we'll create the controller that's supposed to handle the temporary upload of the image to show it in the preview, which is at the URL imageuploader/images/tempUpload. So, create under Controller\Adminhtml\Images the file TempUpload.php with the following content:

<?php

namespace VENDOR\ImageUploader\Controller\Adminhtml\Images;

use Magento\Backend\App\Action\Context;
use Magento\Backend\Model\UrlInterface;
use Magento\Framework\Controller\ResultFactory;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\MediaStorage\Model\File\UploaderFactory;
use Magento\Store\Model\StoreManagerInterface;

class TempUpload extends \Magento\Backend\App\Action {

  /**
   *
   * @var UploaderFactory
   */
  protected $uploaderFactory;

  /** 
   * @var Filesystem\Directory\WriteInterface 
   */
  protected $mediaDirectory;
  
  /**
   * @var StoreManagerInterface
   */
  protected $storeManager;

  public function __construct(
    Context $context,
    UploaderFactory $uploaderFactory,
    Filesystem $filesystem,
    StoreManagerInterface $storeManager
  )
  {
    parent::__construct($context);
    $this->uploaderFactory = $uploaderFactory;
    $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA);
    $this->storeManager = $storeManager;
  }

  public function execute()
  {
    $jsonResult = $this->resultFactory->create(ResultFactory::TYPE_JSON);
    try {
        $fileUploader = $this->uploaderFactory->create(['fileId' => 'image']);
        $fileUploader->setAllowedExtensions(['jpg', 'jpeg', 'png']);
        $fileUploader->setAllowRenameFiles(true);
        $fileUploader->setAllowCreateFolders(true);
        $fileUploader->setFilesDispersion(false);
        $fileUploader->validate();
        $result = $fileUploader->save($this->mediaDirectory->getAbsolutePath('tmp/imageUploader/images'));
        $result['url'] = $this->storeManager->getStore()->getBaseUrl(UrlInterface::URL_TYPE_MEDIA)
            . 'tmp/imageUploader/images/' . ltrim(str_replace('\\', '/', $result['file']), '/');
        return $jsonResult->setData($result);
    } catch (LocalizedException $e) {
        return $jsonResult->setData(['errorcode' => 0, 'error' => $e->getMessage()]);
    } catch (\Exception $e) {
        error_log($e->getMessage());
        error_log($e->getTraceAsString());
        return $jsonResult->setData(['errorcode' => 0, 'error' => __('An error occurred, please try again later.')]);
    }
  }
}

Let's dissect this to understand it better:

  1. We're using uploaderFactory of type Magento\MediaStorage\Model\File\UploaderFactory to first get the uploaded image from the request. We pass it fileId with the value image, which is the name of the field. If it was named something else like "file" then fileId will be file.
  2. We're specifying some rules for validation next. We're setting the allowed extensions with setAllowedExtensions. We're also using setAllowRenameFiles to allow renaming the file uploaded on upload, setAllowCreateFolders to allow creating a folder if it doesn't exist, and setFilesDispersion to disable files dispersion.
  3. Then, we're saving the temporary file to tmp/imageUploader/images under the media directory in pub.
  4. Finally, we're sending back the URL for the uploaded file.

Upload and Save Image

The last thing we need to implement is the Save controller. This controller will handle the submission of the image to finally upload it and save it in our database. To do that, create under Controller\Adminhtml\Images the file Save.php with the following content:

<?php

namespace VENDOR\ImageUploader\Controller\Adminhtml\Images;

use Magento\Backend\App\Action\Context;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Filesystem;
use Magento\Framework\Validation\ValidationException;
use Magento\MediaStorage\Model\File\UploaderFactory;

class Save extends \Magento\Backend\App\Action {
  /**
   *
   * @var UploaderFactory
   */
  protected $uploaderFactory;

  /**
   * @var \VENDOR\ImageUploader\Model\ImageFactory
   */
  protected $imageFactory;

  /** 
   * @var Filesystem\Directory\WriteInterface 
   */
  protected $mediaDirectory;

  public function __construct(
    Context $context,
    UploaderFactory $uploaderFactory,
    Filesystem $filesystem,
    \VENDOR\ImageUploader\Model\ImageFactory $imageFactory
  )
  {
    parent::__construct($context);
    $this->uploaderFactory = $uploaderFactory;
    $this->imageFactory = $imageFactory;
    $this->mediaDirectory = $filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::MEDIA);
  }

  public function execute()
  {
    try {
      if ($this->getRequest()->getMethod() !== 'POST' || !$this->_formKeyValidator->validate($this->getRequest())) {
        throw new LocalizedException(__('Invalid Request'));
    }

    //validate image
    $fileUploader = null;
    $params = $this->getRequest()->getParams();
    try {
        $imageId = 'image';
        if (isset($params['image']) && count($params['image'])) {
            $imageId = $params['image'][0];
            if (!file_exists($imageId['tmp_name'])) {
                $imageId['tmp_name'] = $imageId['path'] . '/' . $imageId['file'];
            }
        }
        $fileUploader = $this->uploaderFactory->create(['fileId' => $imageId]);
        $fileUploader->setAllowedExtensions(['jpg', 'jpeg', 'png']);
        $fileUploader->setAllowRenameFiles(true);
        $fileUploader->setAllowCreateFolders(true);
        $fileUploader->validateFile();
        //upload image
        $info = $fileUploader->save($this->mediaDirectory->getAbsolutePath('imageUploader/images'));
        /** @var \VENDOR\ImageUploader\Model\Image */
        $image = $this->imageFactory->create();
        $image->setPath($this->mediaDirectory->getRelativePath('imageUploader/images') . '/' . $info['file']);
        $image->save();
    } catch (ValidationException $e) {
      throw new LocalizedException(__('Image extension is not supported. Only extensions allowed are jpg, jpeg and png'));
    } catch (\Exception $e) {
        //if an except is thrown, no image has been uploaded
        throw new LocalizedException(__('Image is required'));
    }

    $this->messageManager->addSuccessMessage(__('Image uploaded successfully'));

    return $this->_redirect('*/*/index');
    } catch (LocalizedException $e) {
      $this->messageManager->addErrorMessage($e->getMessage());
      return $this->_redirect('*/*/upload');
    } catch (\Exception $e) {
        error_log($e->getMessage());
        error_log($e->getTraceAsString());
        $this->messageManager->addErrorMessage(__('An error occurred, please try again later.'));
        return $this->_redirect('*/*/upload');
    }

  }
}

This one is pretty similar to TempUpload. Again, let's dissect it:

  1. We're validating the request by checking that the method is POST and the form key is valid.
  2. We're getting the params that are passed to the request.
  3. We're checking if $params['image'] is an image, then we're setting the imageId to the first image. This is because the ImageUploader can accept multiple images.
  4. After that, we're doing the same steps that we did in TempUpload. We're setting some options like the allowed extensions.
  5. We're saving the file in imageUploader/images under the media directory in pub.
  6. We're creating a new instance of Image, which is the model for the table that we created in this module, and we're setting the path to the path of the image uploaded. After that, we're saving the model.
  7. If no errors occur and everything works correctly, we're sending a success message and redirecting back to the index page that shows the image listing.

Our form, all its components, and controllers related to it are ready. We just need to compile our code:

php bin/magento setup:di:compile

After that is done, again, log in to the admin side of your store. Click on ImageUploader -> All Images -> Upload Images. You'll see a form with an image uploader. Try uploading an image by clicking on Upload next to the Images field. The image uploader field will upload the image using the route imageuploader/images/tempupload which will upload the image to pub/media/tmp/imageuploader/images, then returns the URL. Then, the ImageUploader field will show a preview of the file.

Click on the orange button Upload at the top right corner. This will send the form data to the endpoint imageuploader/images/save. This one will save the image under pub/media/imageuploader/images, then save the image with the path in the table we created for the module. If everything is correct, you'll be redirected to the listing page and you'll see the image you just uploaded.

Conclusion

This is how an ImageUploader component works! Next, you can try adding delete functionalities. You can also try adding other fields to the form, if necessary. It will work perfectly well.