statamic / cms

The core Laravel CMS Composer package
https://statamic.com
Other
3.89k stars 521 forks source link

Not able to upload files with form #10047

Closed MrMooky closed 4 months ago

MrMooky commented 4 months ago

Bug description

I am not able to upload files when sending the form via AlpineJS. The error message states that "Dateien" has to be an array. I also tried setting max_files to 1, but then the error states that 1 file is required, even though a file has been added.

Screenshot 2024-05-13 at 10 04 51

While looking for a solution, I stumbled upon this issue, which is essentially the same problem, only that I'm sending via AlpineJS.

This is the JS code:

document.addEventListener('alpine:initializing', () => {
    Alpine.data('formHandler', () => ({
        processing: false,
        success: false,
        form: null,
        init() {
            this.form = this.$form(
                'post',
                this.$refs.form.getAttribute('action'),
                JSON.parse(this.$refs.form.getAttribute('x-data')).form,
                {
                    headers: {
                        'X-CSRF-Token': {
                            toString: () => this.$refs.form.querySelector('[name="_token"]').value,
                        }
                    }
                }
            )
        },
        submit() {
            this.processing = true;
            this.form.submit()
                .then(response => {
                    {{ if (success_action == 'redirect') }}
                        window.location.replace("{{ success_redirect }}");
                    {{ else }}
                        this.processing = false;
                        this.success = true
                        this.$refs.form.reset()
                    {{ /if }}
                })
                .catch(error => {
                    console.log(error);
                    this.processing = false;
                })
        }
    }))
})

This is the rendered field in question:

<input type="file" name="dateien[]" multiple="" x-model="form.dateien" @change="form.validate('dateien')" :aria-invalid="form.invalid('dateien')">

How to reproduce

Create a form and enable attachments. Then use the JS from the bug description to try to send the form.

Logs

No response

Environment

Environment
Application Name: ABC
Laravel Version: 10.48.10
PHP Version: 8.2.16
Composer Version: 2.7.6
Environment: local
Debug Mode: ENABLED
URL: abc.test
Maintenance Mode: OFF

Cache
Config: NOT CACHED
Events: NOT CACHED
Routes: NOT CACHED
Views: CACHED

Drivers
Broadcasting: log
Cache: statamic
Database: mysql
Logs: stack / single
Mail: postmark
Queue: sync
Session: file

Statamic
Addons: 2
Antlers: runtime
Sites: 1
Stache Watcher: Disabled
Static Caching: Disabled
Version: 4.57.3 PRO

Statamic Addons
jacksleight/statamic-bard-texstyle: 3.2.0
mitydigital/sitemapamic: 2.3.9

Installation

Fresh statamic/statamic site via CLI

Additional details

No response

duncanmcclean commented 4 months ago

Form uploads seem to be generally working for me, I've tested in a few sites on the latest version which probably means it's a templating/JS issue.

Can you provide the template where you're using the {{ form }} tag?

ryanmitchell commented 4 months ago

If youre using Alpine in front end forms, you need to index the field... eg

            <input
                id="assets_field"
                name="assets_field[]"
                type="file"
                @change="form.assets_field[0] = $event.target.files[0]; form.validate('assets_field[0]')"
            />

or alternative this should work (untested)

            <input
                id="assets_field"
                name="assets_field[]"
                type="file"
                @change="form.assets_field = $event.target.files; form.validate('assets_field')"
            />

using x-model doesn't work.

MrMooky commented 4 months ago

Can you provide the template where you're using the {{ form }} tag?

Sure, this is the HTML. The partial form_handler (at the bottom) includes the JS from above.


{{ if form:handle }}
    {{ form:create :in="form:handle" js="alpine:form" attr:x-ref="form" }}
        <span class="absolute top-[-300px]" x-ref="anchor" xmlns="http://www.w3.org/1999/html"></span>
            <div x-data="formHandler()" x-cloak class="{{ class }}">
                <div {{ if success_action == 'notification' }}x-show="success === false" x-transition{{ /if }}>

                {{# Honeypot spam protection. #}}
                <div class="hidden">
                    <label class="font-bold" for="{{ honeypot }}">{{ trans:strings.form_honeypot }} <sup class="text-yellow-400">*</sup></label>
                    <input class="w-full form-input" id="{{ honeypot }}" type="text" name="{{ honeypot }}" tabindex="-1" autocomplete="off" />
                </div>

                {{ sections }}
                    <fieldset class="grid w-full gap-6 md:grid-cols-12">
                        {{ if display || instructions }}
                            <span class="md:col-span-12">
                                {{ display ?= {partial:typography/h2 class="mb-2" as="legend" content="{trans :key="display"}"} }}
                                {{ instructions ?= {partial:typography/p content="{trans :key="instructions"}"} }}
                            </span>
                        {{ /if }}

                        {{ fields }}
                            <template x-if="{{ show_field }}">
                                <div class="{{ input_type == 'hidden' ?= 'hidden' }} flex flex-col space-y-3
                                    {{ width == '25' ?= 'md:col-span-3' }}
                                    {{ width == '33' ?= 'md:col-span-4' }}
                                    {{ width == '50' ?= 'md:col-span-6' }}
                                    {{ width == '66' ?= 'md:col-span-8' }}
                                    {{ width == '75' ?= 'md:col-span-9' }}
                                    {{ width == '100' ?= 'md:col-span-12' }}">
                                    <div class="relative">
                                        {{ if type == "toggle" || type == "checkbox" || type == "radio" }}
                                            {{ field }}
                                        {{ elseif type != "hidden" }}
                                            {{ field }}

                                            <label for="{{ handle }}" class="absolute -translate-y-1 scale-75 top-2 peer-focus:px-2 peer-focus:text-blue-600 peer-placeholder-shown:scale-100 peer-placeholder-shown:-translate-y-1/2 peer-placeholder-shown:top-[26px] peer-focus:top-2 peer-focus:scale-75 peer-focus:-translate-y-1 left-1 text-sm text-gray-500 transition-floatinglabels duration-300 transform z-10 origin-[0] px-2">
                                                {{ trans :key="display" }}

                                                {{ if validate | contains('required') }}
                                                    <sup class="font-semibold text-green-500">*</sup>
                                                {{ /if }}

                                                {{ if instructions }}
                                                    {{ partial:typography/p class="my-1 text-sm" content="{trans :key="instructions"}" }}
                                                {{ /if }}
                                            </label>
                                        {{ /if }}
                                    </div>
                                </div>
                            </template>
                        {{ /fields }}
                    </fieldset>
                {{ /sections }}

                <div class="w-full flex justify-end mt-6">
                    <template x-if="processing === false">
                        {{ partial:components/button button_type="btn-primary" as="button" label="{send_action_label ?? 'Nachricht senden'}" }}
                            {{ slot:attributes }} @click.prevent="submit" {{ /slot:attributes }}
                        {{ /partial:components/button }}
                    </template>
                    <template x-if="processing === true">
                        {{ partial:components/button button_type="btn-primary" class="w-full !text-center opacity-75 cursor-default" as="button" label="{ partial:components/loading_circle } Bitte warten" }}
                            {{ slot:attributes }} @click.prevent="submit" disabled {{ /slot:attributes }}
                        {{ /partial:components/button }}
                    </template>
                </div>

                <template x-if="form.hasErrors">
                    <div id="summary" role="group" class="rounded border p-4 bg-red-50 border-red-700 mt-6">
                        <p class="leading-5 text-red-700">Bitte fülle die markierten Felder aus.</p>
                    </div>
                </template>
            </div>

            {{ if success_action == 'notification' }}
                <div id="summary" role="group" class="rounded p-6 mt-6 text-lg text-center leading-6" x-show="success" x-cloak x-transition>
                    {{ success_notification }}
                </div>
            {{ /if }}
        </div>
    {{ /form:create }}

    {{ partial:snippets/form_handler }}
{{ /if }}
duncanmcclean commented 4 months ago

Can you try the fix @ryanmitchell suggested?

MrMooky commented 4 months ago

@ryanmitchell That worked perfectly, thank you very much. :)