Skip to content

Latest commit

 

History

History
369 lines (284 loc) · 11 KB

99_CustomAdapter.md

File metadata and controls

369 lines (284 loc) · 11 KB

Dynamic Multi File | Custom Adapter

Creating a custom adapter is quite easy.

There are 5 steps to go:

  • Evaluate your library
  • Register Services & adjust form builder
  • Create Form Type
  • Create Adapter
  • Create Js Handler

I. Register Services

services:
    _defaults:
        autowire: true
        autoconfigure: true
        public: true

    App\DynamicMultiFile\Adapter\MyUploaderAdapter:
        tags:
            - { name: form_builder.dynamic_multi_file.adapter }

    App\Form\Type\DynamicMultiFile\MyUploaderType:
        public: false
        tags:
            - { name: form.type }

## tell form builder to use your custom adapter
form_builder:
    dynamic_multi_file_adapter: App\DynamicMultiFile\Adapter\MyUploaderAdapter

II. Override default handler path

$('form.formbuilder.ajax-form').formBuilderAjaxManager({
    dynamicMultiFileHandlerOptions: {
        // tell form builder core extension where to lazy load your handler from!
        defaultHandlerPath: '/bundles/app/js/dynamic-multi-file-adapter/lib',
        // other options will be passed to your new handler
        // you can define any options here.
        libPath: 'https://cdnjs.cloudflare.com/ajax/libs/my-3rd-party-lib.min.js',
    },
});

III. Register Services

FormType

<?php

namespace App\Form\Type\DynamicMultiFile;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class MyUploaderType extends AbstractType
{
    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
         $view->vars = array_merge_recursive($view->vars, [
            'attr' => [
                'data-field-id'       => $view->parent->vars['id'],
                'data-engine-options' => json_encode([
                    'multiple'           => isset($options['multiple']) ? $options['multiple'] : false,
                    'max_file_size'      => is_numeric($options['max_file_size']) && $options['max_file_size'] > 0 ? $options['max_file_size'] : null,
                    'item_limit'         => is_numeric($options['item_limit']) && $options['item_limit'] > 0 ? $options['item_limit'] : null,
                    'allowed_extensions' => is_array($options['allowed_extensions']) ? $options['allowed_extensions'] : null,
                ]),
                'class'               => join(' ', [
                    'dynamic-multi-file',
                    sprintf('element-%s', $view->vars['name'])
                ])
            ]   
        ]);
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        // these options are required to support!
        
        $resolver->setDefaults([
            'max_file_size'             => null,
            'allowed_extensions'        => [],
            'item_limit'                => null,
            'submit_as_attachment'      => false,
            'submit_as_admin_deep_link' => false
        ]);
    }

    public function getBlockPrefix(): string
    {
        return 'form_builder_dynamicmultifile_my_uploader';
    }

    public function getParent(): string
    {
        return TextType::class;
    }
}

Adapter

<?php

namespace App\DynamicMultiFile\Adapter;

use App\Form\Type\DynamicMultiFile\MyUploaderType;
use FormBuilderBundle\DynamicMultiFile\Adapter\DynamicMultiFileAdapterInterface;
use FormBuilderBundle\Stream\FileStreamInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class MyUploaderAdapter implements DynamicMultiFileAdapterInterface
{
    protected FileStreamInterface $fileStream;

    public function __construct(FileStreamInterface $fileStream)
    {
        $this->fileStream = $fileStream;
    }

    public function getForm(): string
    {
        return MyUploaderType::class;
    }

    public function getJsHandler(): string
    {
        return 'my-uploader';
    }

    public function onUpload(Request $request): Response
    {
        // second argument needs to be false, if you also invoke the onDone action!
        
        $result = $this->fileStream->handleUpload([
            'binary'          => 'REQUEST_PARAM', // replace REQUEST_PARAM with the mapped request parameter, provided in your JS library
            'uuid'            => 'REQUEST_PARAM',
            'chunkIndex'      => 'REQUEST_PARAM',
            'totalChunkCount' => 'REQUEST_PARAM',
            'totalFileSize'   => 'REQUEST_PARAM',
        ], true);

        return new JsonResponse($result);
    }

    public function onDone(Request $request): Response
    {
        // if you need a dedicated endpoint for a chunked upload completion event,
        // use this action (which is mostly not required)
        
        return new JsonResponse([
            'success' => false,
            'message' => 'not implemented'
        ], 403);
    }

    public function onDelete(Request $request): Response
    {
        // $identifier = mostly the upload uuid
        $identifier = $request->attributes->get('identifier');
        
        // some libraries will trigger this action after user cancels an active (chunked) file upload
        $checkChunkFolder = $request->request->get('uploadStatus') === 'canceled';

        $result = $this->fileStream->handleDelete($identifier, $checkChunkFolder);

        return new JsonResponse($result);
    }
}

IV. Twig Form Theme

{% block form_builder_dynamicmultifile_my_uploader_widget %}
    {% apply spaceless %}
        <div {{ block('attributes') }}>
            <div class="my-uploader-container">{# your adapter markup #}</div>
        </div>
    {% endapply %}
{% endblock %}

V. JavaScript Handler

// assets/js/dynamic-multi-file-adapter/lib/jquery.fb.dmf.my-uploader.js
;(function ($, window) {

    'use strict';

    function FormBuilderDynamicMultiFileMyUploader() {
        return this;
    }

    $.extend(FormBuilderDynamicMultiFileMyUploader.prototype, {

        init: function ($form, $dmfFields, dataUrls, options) {

            if (this.initialized === true) {
                return;
            }

            this.$form = $form;
            this.$dmfFields = $dmfFields;
            this.dataUrls = dataUrls;
            this.options = options;
            this.initialized = true;

            this.prepareLibrary();
        },

        prepareLibrary: function () {

            if (window.myUploaderLibrary !== undefined) {
                this.prepareForm();
                return;
            }

            if (typeof this.options.libPath === 'undefined') {
                return;
            }

            $.getScript(this.options.libPath, function (data, textStatus, jqxhr) {
                if (jqxhr.status === 200) {
                    this.prepareForm();
                }
            }.bind(this));

        },

        prepareForm: function () {
            this.addListener();
            this.$dmfFields.each(this.setupMyUploadElement.bind(this));
        },

        setupMyUploadElement: function (index, el) {

            var _ = this,
                $el = $(el),
                $element = $el.find('.my-uploader-container'),
                fieldId = $el.data('field-id'),
                storageFieldId = fieldId + '_data',
                $storageField = this.$form.find('input[type="hidden"][id="' + storageFieldId + '"]'),
                config = $el.data('engine-options'),
                myUploadConfiguration;

            // this is just an example configuration, 
            // check the corresponding docs provided by your js lib
            
            myUploadConfiguration = {
                addUrl: _.getDataUrl('file_add'),
                deleteUrl: _.getDataUrl('file_delete'),
                doneUrl: _.getDataUrl('file_chunk_done'),
                onAdd: function(response) {
                    this.addToStorageField($storageField, {
                        id: response.uuid,
                        fileName: response.fileName
                    });
                },
                onRemove: function(response) {
                    this.removeFromStorageField($storageField, {
                        id: response.uuid
                    });
                }
            };

            // allow 3rd party hooks
            this.$form.trigger('formbuilder.dynamic_multi_file.init', [$el, this.options, myUploadConfiguration]);

            $element.myUploaderLibrary(myUploadConfiguration);
        },

        addToStorageField: function ($storage, newData) {

            var data = typeof $storage.val() === 'string' && $storage.val() !== ''
                ? $.parseJSON($storage.val())
                : [];

            data.push(newData);

            $storage.val(JSON.stringify(data));
        },

        removeFromStorageField: function ($storage, newData) {

            var position,
                data = typeof $storage.val() === 'string' && $storage.val() !== ''
                    ? $.parseJSON($storage.val())
                    : [];

            position = $.map(data, function (block) {
                return block.id
            }).indexOf(newData.id);

            if (position === -1) {
                return;
            }

            data.splice(position, 1)

            $storage.val(JSON.stringify(data));
        },

        addListener: function () {

            this.$form.on({
                // add events for repeater fields!
                'formbuilder.layout.post.add': function (ev, layout) {

                    var $el = $(layout),
                        $instances;

                    $instances = $el.find('[data-dynamic-multi-file-instance]');
                    if ($instances.length === 0) {
                        return;
                    }

                    $instances.each(this.setupMyUploadElement.bind(this));

                }.bind(this),
                'formbuilder.layout.pre.remove': function (ev, layout) {

                    var $el = $(layout),
                        $uploadFields;

                    $uploadFields = $el.find('[data-dynamic-multi-file-instance]');
                    if ($uploadFields.length === 0) {
                        return;
                    }

                    $uploadFields.each(function (index, el) {

                        var $el = $(el),
                            config = $el.data('engine-options');

                        // check if field has an active upload process.
                        // if so, throw an error which will be catched via alert:

                        // throw new Error('Cannot remove this block.');
                    });
                }
            });
        },

        getDataUrl: function (section) {
            return this.dataUrls[section];
        }
    });

    // window instance requires to be called "formBuilderDynamicMultiFileHandler"
    window.formBuilderDynamicMultiFileHandler = new FormBuilderDynamicMultiFileMyUploader();

})(jQuery, window);