5

Short story:

I have a form in my admin in which I can dynamically add multiple file upload fields in the admin. This mechanism is built with Knockout. But when I submit the form the files are not uploaded (since Magento 'transfers' the form information to a hidden form that gets submitted instead). How can I make it so that dynamically added file upload fields are taken into account when submitting?

Long story:

I've created a component in Knockout that can create a set of options. These options have a label and an image. Under water, all this configuration is saved into a JSON-string, and this JSON string is stored in the database. This works perfectly, except for the images.

I've already tried this solution but these aren't dynamically added upload fields. Nevertheless, adding a dummy upload field to my form will convert Magento's 'hidden' form to enctype="multipart/form-data", but it will not include my dynamically added file upload fields.

First step: I added a custom field in my UI Components' XML:

<field name="configuration">
    <argument name="data" xsi:type="array">
        <item name="config" xsi:type="array">
            <!-- Change the component: -->
            <item name="component" xsi:type="string">Vendor_Module/js/form/element/configurator</item>
            <!--main template for form field that renders elementTmpl as a child template-->
            <item name="template" xsi:type="string">ui/form/field</item>
            <!-- Set the template :-->
            <item name="elementTmpl" xsi:type="string">Vendor_Module/form/element/configurator</item>
            <item name="dataType" xsi:type="string">text</item>
            <item name="label" xsi:type="string" translate="true">Configuration</item>
            <item name="formElement" xsi:type="string">textarea</item>
            <item name="source" xsi:type="string">form</item>
            <item name="sortOrder" xsi:type="number">30</item>
            <item name="dataScope" xsi:type="string">configuration</item>
            <item name="validation" xsi:type="array">
                <item name="required-entry" xsi:type="boolean">true</item>
            </item>
        </item>
    </argument>
</field>

The html-template that renders the input fields and provides a button for adding a new options looks like this:

<textarea class="admin__control-textarea" data-bind="
    value: value,
    valueUpdate: valueUpdate,
    hasFocus: focused,
    attr: {
        name: inputName,
        cols: cols,
        rows: rows,
        'aria-describedby': noticeId,
        placeholder: placeholder,
        id: uid,
        disabled: disabled
    }"
/>

... some knockout template code ... 

<div class="form-configurator__options">
    <label data-bind="i18n: 'Options'"></label>
    <ul data-bind="foreach:options">
        <li>
            <input type="text" data-bind="value:value"/>
            <input type="text" data-bind="value:image"/>
            <input type="file" data-bind="attr:{name:'image_' + $parentContext.$parentContext.$index() + '_' + $parentContext.$index() + '_' + $index()}"/>
        </li>
    </ul>
    <a href="#" class="form-configurator__button" data-bind="i18n: 'Add New Value', click: $parents[1].addOption.bind($parents[1])"></a>
</div>

... some knockout template code ... 

The name of the input field is dynamically built according to it's scope, but it will always be something like image_1_0_0 or image_1_2_3 etc. The options is an observable array and addOption adds a new option to this array:

addOption: function(column) {
  column.options.push({
    value: this.getSubscribedObservable(),
    image: this.getSubscribedObservable()
  });
},

getSubscribedObservable() is a method that returns the same as ko.observable() but subscribes an event to it that updates the value() observable of my UIElement, effectively replacing the text in the textarea with the JSON string (as I mentioned above).

Now, because everything is transferred into the initial textarea (with the name configuration), everything works well. But the file upload fields of course cannot be processed like this.

So the only thing I want to do is make sure these files are posted to the server, so I can handle the file uploads from within my controller (I can match them with the correct option according to their dynamic names (image_0_0_1, image_0_0_2, etc.). So the question here is: how can I get those files posted to my server?

When I submit the form, Magento quickly creates a hidden form, but I think it only looks at the fields that are declared in my UI Component, not the fields that are dynamically added with JavaScript. So how can I do this?

Or even a more basic example that leaves file uploads out of the equation: "If a form adds an extra text input field that is not available in the UI Component, how can it get submitted to my controller?"

I already figured out how to do this for default form fields (like text inputs for example. By adding a data-form-part-attribute to the element in question, it gets added to the 'hidden' form:

<input type="text"
       data-form-part="form_form_form"
       data-bind="attr:{name:'image_' + $parentContext.$parentContext.$index() + '_' + $parentContext.$index() + '_' + $index()}"/>

But this doesn't work for file-input fields, since they would just be transformed to text-fields and get the fake value, but no file upload (for example: "C:\fakepath\some-image.png").

Any help would be greatly appreciated.

  • can you add some code? how is your form built? – Marius Jan 11 '17 at 10:13
  • It's quite a large project, but I try to pin provide a simplified example. – Giel Berkers Jan 11 '17 at 10:20
  • at least the code to see how the image fields are generated maybe? – Marius Jan 11 '17 at 10:22
  • I updated the question. – Giel Berkers Jan 11 '17 at 10:30
2

It took a while, but I'm finally able to answer my own question. As I stated, the content of a form is copied to a hidden form, and only the values of the fields that are declared in the UI Components' XML file are copied. Additional fields can be added with the data-form-part-attribute.

The first thing we need to do, is figure out, where in the code these values are copied to this hidden form. Well, turns out this happens in lib/mage/utils/misc.js::submit():

submit: function (options, attrs) {
    var form        = document.createElement('form'),
        data        = this.serialize(options.data),
        attributes  = _.extend({}, defaultAttributes, attrs || {}),
        field;

    if (!attributes.action) {
        attributes.action = options.url;
    }

    data['form_key'] = window.FORM_KEY;

    _.each(attributes, function (value, name) {
        form.setAttribute(name, value);
    });

    _.each(data, function (value, name) {
        field = document.createElement('input');

        field.setAttribute('name', name);
        field.setAttribute('type', 'hidden');

        field.value = value;

        form.appendChild(field);
    });

    document.body.appendChild(form);

    form.submit();
},

As you can see, all attributes that are passed to this function are transformed into hidden input elements in a new form. But as we all know, if you want to submit files, you need type="file", not hidden. So we need to override this function.

So first we need to look where it's declared. After some backtracing, I noticed that the submit()-method is mostly used from the mageUtils-component.

What is this mageUtils you might ask? Well, it's a requirejs map set by the theme module. It loads lib/mage/utils/main.js which is a placeholder for all the utils:

define(function (require) {
    'use strict';

    var utils = {},
        _ = require('underscore');

    return _.extend(
        utils,
        require('./arrays'),
        require('./compare'),
        require('./misc'),
        require('./objects'),
        require('./strings'),
        require('./template')
    );
});

So let's override this file. I did this by adding a requirejs-config.js to my modules' view/adminhtml-folder:

var config = {
    "map": {
        "*": {
            "mageUtils": "Vendor_Module/js/mage/utils/main"
        }
    }
};

And here's my modified main.js:

/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
define(function (require) {
    'use strict';

    var utils = {},
        _ = require('underscore');

    return _.extend(
        utils,
        require('mage/utils/arrays'),
        require('mage/utils/compare'),
        require('mage/utils/misc'),
        require('mage/utils/objects'),
        require('mage/utils/strings'),
        require('mage/utils/template'),
        // Overrule submit-method, so it includes file uploads:
        {
            /**
             * Serializes and sends data via POST request.
             *
             * @param {Object} options - Options object that consists of
             *      a 'url' and 'data' properties.
             * @param {Object} attrs - Attributes that will be added to virtual form.
             */
            submit: function (options, attrs) {
                var defaultAttributes = {
                    method: 'post',
                    enctype: 'multipart/form-data'
                };

                var form        = document.createElement('form'),
                    data        = this.serialize(options.data),
                    attributes  = _.extend({}, defaultAttributes, attrs || {}),
                    field;

                if (!attributes.action) {
                    attributes.action = options.url;
                }

                data['form_key'] = window.FORM_KEY;

                _.each(attributes, function (value, name) {
                    form.setAttribute(name, value);
                });

                var fileUploadField;

                _.each(data, function (value, name) {
                    field = document.createElement('input');

                    // Check if this is a file upload field:
                    if (fileUploadField = document.querySelector('input[type="file"][name="' + name + '"]')) {
                        // field = fileUploadField.clone();
                        form.appendChild(fileUploadField);
                    } else {
                        field.setAttribute('name', name);
                        field.setAttribute('type', 'hidden');
                        field.value = value;
                        form.appendChild(field);
                    }
                });

                document.body.appendChild(form);

                form.submit();
            }
        }
    );
});

As you can see, I include all the default stuff, just like the original main.js, but I add an extra option, only containing a new submit()-method, effectively overriding the one set by misc.js. The only addition I did, was check if there is a file upload field that has the same name. If so, move (not clone, since that doesn't work for security purposes) the input field to the new form, otherwise fall back to your default behavior.

It took a while, but it gets the job done. Now I am able to submit file uploads to the server with dynamically added upload fields. Yay!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.