Question

I'm building a CRUD module for Magento 2 using ui components for admin list and form and one of my entities has an image field.
But I cannot make it work as it should.
Here is how it should work.
When in add mode or in edit mode with no uploaded image it should look like a simple file input.

When a file is uploaded it should show the image preview and a delete box below it.

I'm not looking for the exactly this design. It could look differently but have the same functionality.

In Magento 1 I was able to do this, just by creating my own block renderer

class {{Namespace}}_{{Module}}_Block_Adminhtml_{{Entity}}_Helper_Image extends Varien_Data_Form_Element_Image
{
    protected function _getUrl()
    {
        $url = false;
        if ($this->getValue()) {
            $url = Mage::helper('{{namespace}}_{{module}}/{{entity}}_image')->getImageBaseUrl().$this->getValue();
        }
        return $url;
    }
}

And adding this in my form block

    $fieldset->addType(
        'image',
        Mage::getConfig()->getBlockClassName('{{namespace}}_{{module}}/adminhtml_{{entity}}_helper_image')
    );

But I have no form block in Magento 2.
I know I can use a class name for a form field in the ui components file

    <field name="image" class="Class\Name\Here">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="dataType" xsi:type="string">text</item>
                <item name="label" xsi:type="string" translate="true">Resume</item>
                <item name="formElement" xsi:type="string">image</item>
                <item name="source" xsi:type="string">[entity]</item>
                <item name="dataScope" xsi:type="string">image</item>
            </item>
        </argument>
    </field>

Obviously I have to create this class, but what should I extend?
All I know is that I need to implement the interface Magento\Framework\View\Element\UiComponentInterface but I found nothing I can extend.
So my real question is: Can I extend some class to achieve the desired behavior? If not, how can I start creating this element renderer?

Was it helpful?

Solution

I found a way to do it without requiring a class attached to the field. I mean there is a class attached to the form element but not as a renderer.
The column should be defined as this:

<field name="image">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <item name="dataType" xsi:type="string">string</item>
            <item name="source" xsi:type="string">[entity]</item>
            <item name="label" xsi:type="string" translate="true">Image</item>
            <item name="visible" xsi:type="boolean">true</item>
            <item name="formElement" xsi:type="string">fileUploader</item>
            <item name="elementTmpl" xsi:type="string">ui/form/element/uploader/uploader</item>
            <item name="previewTmpl" xsi:type="string">[Namespace]_[Module]/image-preview</item>
            <item name="required" xsi:type="boolean">false</item>
            <item name="uploaderConfig" xsi:type="array">
                <item name="url" xsi:type="url" path="[namespace_module]/[entity]_image/upload"/>
            </item>
        </item>
    </argument>
</field>

I also needed to create the preview template file referenced by [Namespace]_[Module]/image-preview.
That is app/code/[Namespace]/[Module]/view/adminhtml/web/template/image-preview.html that looks like this:

<div class="file-uploader-summary">
    <div class="file-uploader-preview">
        <a attr="href: $parent.getFilePreview($file)" target="_blank">
            <img
                class="preview-image"
                tabindex="0"
                event="load: $parent.onPreviewLoad.bind($parent)"
                attr="
                    src: $parent.getFilePreview($file),
                    alt: $file.name">
        </a>

        <div class="actions">
            <button
                type="button"
                class="action-remove"
                data-role="delete-button"
                attr="title: $t('Delete image')"
                click="$parent.removeFile.bind($parent, $file)">
                <span translate="'Delete image'"/>
            </button>
        </div>
    </div>

    <div class="file-uploader-filename" text="$file.name"/>
    <div class="file-uploader-meta">
        <text args="$file.previewWidth"/>x<text args="$file.previewHeight"/>
    </div>
</div>

This code will generate a field like this:

After uploading an image (real time) it looks like this:

The url item inside the uploaderConfig is the url where the image is posted when uploaded. So I needed to create this also:

namespace [Namespace]\[Module]\Controller\Adminhtml\[Entity]\Image;

use Magento\Framework\Controller\ResultFactory;

/**
 * Class Upload
 */
class Upload extends \Magento\Backend\App\Action
{
    /**
     * Image uploader
     *
     * @var \[Namespace]\[Module]\Model\ImageUploader
     */
    protected $imageUploader;

    /**
     * @param \Magento\Backend\App\Action\Context $context
     * @param \[Namespace]\[Module]\Model\ImageUploader $imageUploader
     */
    public function __construct(
        \Magento\Backend\App\Action\Context $context,
        \[Namespace]\[Module]\Model\ImageUploader $imageUploader
    ) {
        parent::__construct($context);
        $this->imageUploader = $imageUploader;
    }

    /**
     * Check admin permissions for this controller
     *
     * @return boolean
     */
    protected function _isAllowed()
    {
        return $this->_authorization->isAllowed('[Namespace]_[Module]::[entity]');
    }

    /**
     * Upload file controller action
     *
     * @return \Magento\Framework\Controller\ResultInterface
     */
    public function execute()
    {
        try {
            $result = $this->imageUploader->saveFileToTmpDir('image');

            $result['cookie'] = [
                'name' => $this->_getSession()->getName(),
                'value' => $this->_getSession()->getSessionId(),
                'lifetime' => $this->_getSession()->getCookieLifetime(),
                'path' => $this->_getSession()->getCookiePath(),
                'domain' => $this->_getSession()->getCookieDomain(),
            ];
        } catch (\Exception $e) {
            $result = ['error' => $e->getMessage(), 'errorcode' => $e->getCode()];
        }
        return $this->resultFactory->create(ResultFactory::TYPE_JSON)->setData($result);
    }
}

This class uses an instance of [Namespace]\[Module]\Model\ImageUploader that is similar to \Magento\Catalog\Model\ImageUploader.

This seams to work. I still have troubles saving the image in the db but that's a totally different issue.
I used as inspiration the image field for the category entity

OTHER TIPS

Yes, the class you should extend is \Magento\Ui\Component\Form\Element\AbstractElement.

This class implements the ElementInterface which itself extends the UiComponentInterface you're referring to.

On top of that, if you check the components declared under Magento\Ui\Component\Form\Element you can see that they all extend that class.

The reason I would choose this class is because the render method of \Magento\Backend\Block\Widget\Form\Renderer\Element only accepts such class type: (This is actually an instance of Magento\Framework\Data\Form\Element\AbstractElement that is accepted, not \Magento\Ui\Component\Form\Element\AbstractElement )

Licensed under: CC-BY-SA with attribution
Not affiliated with magento.stackexchange
scroll top