inertiajs / inertia-laravel

The Laravel adapter for Inertia.js.
https://inertiajs.com
MIT License
2.05k stars 230 forks source link

PreserveScroll not working without PreserveState for inertia form #642

Closed buckfuddey closed 3 months ago

buckfuddey commented 3 months ago

Version:

Describe the problem:

So to preface this, I'm rather new to front-end frameworks overall and might just be crying wolf. However, I can't for the life of me find a line of documentation or reference in a forum that states whether preserveScroll is supposed to work without preserveState.

My issues is that I have a file component which does this:

const form = useForm({
    file: null
})
const submit = () => {
    form.post(
        route('file.store'),
        {
            preserveScroll: false,
            preserveState: false
        }
    );
}

If I set preserveScroll: true, preserveState seems to default to true, as my refs retain their pre-submit value and my form is not reset.

If I set preserveScroll: true, preserveState: false, the page reloads and jumps to the top after the request is successful.

I have opted to set a

setTimeout(function () {
    scrollRegion.value.scrollIntoView()
}, 100);

To mitigate loosing valuable feedback from the backend on failure or success. However this leaves the form feeling clunky as it jumps abruptly, looking kind of glitchy. I'm also worried that users who have slow internet may enter a state where the component is not rendered in time, because without it, Inertia does not know of the element it is supposed to scroll to.

Can someone please let me know if this is expected behavior?

Steps to reproduce:

I'll include my current fix, which also depends on a CreateFile component, the first code block is the CreateFile which only emits, the second is the Component I'm struggling with. Throw these components together, maybe exclude heroicons if its too much hassle to download, as well as the updatesuccess and input error components might cause hassle.

Include the second component in a page you want to try this in and point it to a route that expects a file.

<script setup>
import { onUnmounted } from 'vue';
import { onMounted } from 'vue';
import { ref } from 'vue';

// ICONS
import { DocumentArrowUpIcon } from "@heroicons/vue/24/outline";
import { XCircleIcon } from "@heroicons/vue/24/solid";

const props = defineProps({
    modelValue: {
        default: null
    },
    inputIdAppend: {
        /**
         * If component exists more than once on one page, it will always go top to bottom
         * when inserting files, e.g if you have one file upload in the start and one in the end:
         * if first input is empty and you upload the end first, the file will go to the first one.
         * This is because you need unique id and label for input fields......
         */
        required: true
    },
    file_name: {
        type: String,
        default: 'file'
    }
});

const inputId = 'createfile_file-input-hidden' + (props.inputIdAppend ? props.inputIdAppend : '')

const input = ref(null);

const emit = defineEmits(['update:modelValue']);

const onDrop = function (e) {
    input.value = e.dataTransfer.files[0]
    emit('update:modelValue', e.dataTransfer.files[0], props.inputIdAppend, props.file_name)
}

const onInputChange = function (e) {
    input.value = e.target.files[0]
    emit('update:modelValue', e.target.files[0], props.inputIdAppend, props.file_name)
}

const removeFile = function () {
    input.value = null
    emit('update:modelValue', null, props.inputIdAppend, props.file_name)
}

const preventDefaults = function (e) {
    e.preventDefault()
}

const events = ['dragenter', 'dragover', 'dragleave', 'drop'];

onMounted(() => {
    events.forEach((eventName) => {
        document.body.addEventListener(eventName, preventDefaults)
    })
})

onUnmounted(() => {
    events.forEach((eventName) => {
        document.body.removeEventListener(eventName, preventDefaults)
    })
})

</script>
<template>
    <div v-if="input !== null" class="bg-gray-50 rounded-lg flex py-2 px-6 border items-center relative">
        <div class="mr-2">
            <svg width="31" height="40" viewBox="0 0 31 40" fill="none" xmlns="http://www.w3.org/2000/svg">
                <path fill-rule="evenodd" clip-rule="evenodd"
                    d="M2.49998 0H21.7412L30.9999 9.215V37.5C30.9999 38.8812 29.88 40 28.4999 40H2.49998C1.11994 40 0 38.8812 0 37.5V2.49998C0 1.11877 1.12007 0 2.49998 0Z"
                    fill="white" />
                <path
                    d="M2.49998 0.5H21.5348L30.4999 9.4228V37.5C30.4999 38.6049 29.604 39.5 28.4999 39.5H2.49998C1.39594 39.5 0.5 38.6049 0.5 37.5V2.49998C0.5 1.39508 1.39604 0.5 2.49998 0.5Z"
                    stroke="#E8E9EB" />
                <path d="M30.5001 9.5H23.7294C22.4987 9.5 21.5 8.50231 21.5 7.27286V0.5" stroke="#E8E9EB" />
                <path
                    d="M9.36194 30C9.13579 30 8.91874 29.9316 8.73353 29.8025C8.05704 29.3295 7.96606 28.8031 8.00895 28.4446C8.12722 27.4585 9.43537 26.4264 11.8983 25.3749C12.8757 23.3786 13.8056 20.9188 14.3599 18.8637C13.7114 17.5482 13.081 15.8413 13.5405 14.8401C13.7016 14.4894 13.9025 14.2205 14.2774 14.1042C14.4256 14.0581 14.7999 14 14.9377 14C15.2652 14 15.5531 14.3931 15.7571 14.6354C15.9488 14.8631 16.3836 15.3459 15.5147 18.7553C16.3907 20.4416 17.632 22.1593 18.8212 23.3356C19.6731 23.192 20.4062 23.1187 21.0034 23.1187C22.021 23.1187 22.6378 23.3398 22.8893 23.7953C23.0972 24.172 23.0121 24.6124 22.6358 25.1036C22.2738 25.5754 21.7748 25.825 21.1931 25.825C20.4029 25.825 19.4827 25.3598 18.4566 24.4409C16.613 24.8001 14.46 25.4409 12.7197 26.1502C12.1764 27.2247 11.6559 28.0902 11.1711 28.725C10.505 29.5942 9.93056 30 9.36194 30ZM11.0918 26.8952C9.70311 27.6227 9.13709 28.2205 9.09615 28.5572C9.08965 28.613 9.07211 28.7595 9.37624 28.9764C9.47306 28.9479 10.0384 28.7075 11.0918 26.8952ZM19.9539 24.2047C20.4835 24.5845 20.6128 24.7765 20.9592 24.7765C21.1113 24.7765 21.5447 24.7704 21.7455 24.5094C21.8423 24.3828 21.88 24.3016 21.895 24.258C21.815 24.2187 21.7091 24.1387 21.1314 24.1387C20.8032 24.1393 20.3906 24.1526 19.9539 24.2047ZM15.0995 20.218C14.6348 21.7165 14.0214 23.3343 13.3618 24.7995C14.72 24.3083 16.1964 23.8795 17.5832 23.576C16.7059 22.6263 15.8293 21.4403 15.0995 20.218ZM14.705 15.0927C14.6413 15.1127 13.8407 16.1569 14.7674 17.0406C15.3841 15.7595 14.733 15.0842 14.705 15.0927Z"
                    fill="#E2574C" />
            </svg>
        </div>
        <div class="grow">
            <div class="group">
                <div class="group-hover:hidden">
                    <span v-if="input.name && input.name.length <= 12">
                        {{ input.name }}
                    </span>
                    <span v-else>
                        {{ input.name.substring(0, 9) }}...{{
                            input.name.substring(input.name.lastIndexOf('.') + 1) }}
                    </span>

                </div>
                <div class="hidden group-hover:flex break-all">
                    {{ input.name }}
                </div>
            </div>
            <div class="text-gray-500 text-sm">{{ input.size }}</div>
        </div>
        <div class="absolute -right-2 -top-2 cursor-pointer">
            <XCircleIcon @click="removeFile" class="h-6 w-6 text-red-600" />
        </div>
    </div>

    <div v-if="input === null" @drop.prevent="onDrop" 
    class="bg-gray-50 border-2 py-6 border-dotted rounded-md flex items-center justify-center">
        <label :for="inputId" class="flex flex-col gap-y-4">
            <div class="flex grow justify-center">
                <DocumentArrowUpIcon class="h-10 w-10 text-gray-600" />
            </div>
            <div class="text-center p-2">
                Släpp för att ladda upp <b>en </b>fil
                <br>eller<br>
                <input type="file" :id="inputId" @change="onInputChange" hidden>
            </div>
            <div class="flex flex-col">

                <div class="items-center self-center cursor-pointer w-fit border-[1px] border-blue-500 px-4 py-2 uppercase bg-transparent rounded-md text-normal text-sm font-bold text-blue-500 hover:text-blue-400 tracking-widest hover:border-blue-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
                    bläddra
                </div>

                <p class="text-xs text-center text-gray-500 mt-2">Filtyper som stöds: PDF, PNG, JPG</p>
            </div>
        </label>
    </div>
</template>
<script setup>

import { Link, useForm } from '@inertiajs/vue3';
import InputError from '../InputError.vue';
import { ref } from 'vue';
import CreateFile from './CreateFile.vue';
import UpdateSuccess from '../Popups/UpdateSuccess.vue';

import { XCircleIcon } from "@heroicons/vue/24/solid";

const props = defineProps({
    uploaded_file: {
        type: Object,
    },
    label: {
        type: String, 
        default: ''
    },
    error: {
        type: Object
    },
    inputIdAppend: {
        /**
         * If component exists more than once on one page, it will always go top to bottom
         * when inserting files, e.g if you have one file upload in the start and one in the end:
         * if first input is empty and you upload the end first, the file will go to the first one.
         * This is because you need unique id and label for input fields......
         */
        required: true
    },
})

const form = useForm({
    file: null
})

const submit = () => {
    form.post(
        route('file.store'),
        {
            preserveScroll: false,
            preserveState: false
        }
    );
}

const destroy = () => {
    form.delete(
        route('file.delete', { uploadedFile: props.uploaded_file.id}),
        {
            preserveScroll: true
        }
    )
}

const showError = ref(false)
let errorMessage = '';

const showSuccess = ref(false)
let successMessage = '';

const inputIdAppend =  props.inputIdAppend;

const scrollRegion = ref(null);

const handleSuccess = (key, message) => {

    const rightKey = `success`;

    if (key === rightKey) {

        successMessage = message;
        showSuccess.value = true;

        setTimeout(function () {
            scrollRegion.value.scrollIntoView()
        }, 100);

        setTimeout(function () {
            showSuccess.value = false;
        }, 2000);
    }
}

const handleError = (key, message) => {

    const rightKey = `error`;

    if (key === rightKey) {

        errorMessage = message;
        showError.value = true;

        console.log(showError.value)

        setTimeout(function () {
            scrollRegion.value.scrollIntoView()
        }, 100);

        setTimeout(function () {
            showError.value = false;
        }, 4000);
    }
}

if (props.error && Object.keys(props.error).length > 0) {

    for (let key in props.error) {
        let splitKey = key.split('_');

        if (splitKey.length !== 0) {
            if (splitKey[0] == 'success') {
                handleSuccess(key, props.error[key])
            }

            if (splitKey[0] == 'error') {
                handleError(key, props.error[key])
            }
        }
    }
}

const confirmDeleteActive = ref(false);

const toggleConfirmDelete = function () {
    confirmDeleteActive.value = !confirmDeleteActive.value
}
/** requires wrapping div so that it has a root node */
</script>
<template>    
    <div> 
        <div class="block font-medium text-sm text-gray-700 mb-2" ref="scrollRegion">{{ label }}</div>
        <div v-if="uploaded_file">
            <div v-if="!confirmDeleteActive" class="bg-gray-50 rounded-lg flex py-2 px-6 border items-center relative">

                <div class="absolute -right-2 -top-2 cursor-pointer">
                    <XCircleIcon @click="toggleConfirmDelete()" class="h-6 w-6 text-red-600" />
                </div>

                <div class="grow pr-2">
                    <div class="group">
                        <div class="group-hover:hidden">
                            <span v-if="props.uploaded_file.name && props.uploaded_file.name.length <= 12">
                                {{ props.uploaded_file.name }}
                            </span>
                            <span v-else>
                                {{ props.uploaded_file.name.substring(0, 9) }}...{{
                                props.uploaded_file.name.substring(props.uploaded_file.name.lastIndexOf('.') + 1) }}
                            </span>

                        </div>
                        <div class="hidden group-hover:flex break-all">
                            {{ props.uploaded_file.name }}
                        </div>
                    </div>
                    <div v-if="props.uploaded_file.size_readable" class="text-[#8A9099]">{{ uploaded_file.size_readable }}
                    </div>
                </div>

                <a :href="route('file.download', { uploadedFile: uploaded_file.id})">
                    <div class="grow flex justify-end pr-2">

                        <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <path fill-rule="evenodd" clip-rule="evenodd"
                                d="M12.8989 9.75503C13.1994 9.48146 13.6647 9.50325 13.9383 9.8037C14.2118 10.1041 14.1901 10.5695 13.8896 10.8431L9.99852 14.3861L6.10744 10.8431C5.80699 10.5695 5.7852 10.1041 6.05877 9.8037C6.33235 9.50325 6.79768 9.48146 7.09813 9.75503L9.26278 11.726V2.73574C9.26278 2.3294 9.59218 2 9.99852 2C10.4049 2 10.7343 2.3294 10.7343 2.73574V11.726L12.8989 9.75503ZM3.45455 14.3633C3.45455 13.9617 3.12893 13.6361 2.72727 13.6361C2.32561 13.6361 2 13.9617 2 14.3633V15.333C2 16.8058 3.19391 17.9997 4.66667 17.9997H15.3333C16.8061 17.9997 18 16.8058 18 15.333V14.3633C18 13.9617 17.6744 13.6361 17.2727 13.6361C16.8711 13.6361 16.5455 13.9617 16.5455 14.3633V15.333C16.5455 16.0025 16.0028 16.5451 15.3333 16.5451H4.66667C3.99723 16.5451 3.45455 16.0025 3.45455 15.333V14.3633Z"
                                fill="#3F434A" />
                        </svg>

                    </div>
                </a>

            </div>
            <div v-else class="bg-[#F8F8F8] rounded-lg p-2 items-center">
                <div class="text-center mb-3 font-bold italic">
                    Är du säker?
                </div>
                <div class="flex gap-2">

                    <div class="w-1/2 text-center">
                        <form @submit.prevent="destroy">
                            <button class="bg-green-500 ml-1 max-w-36 rounded-lg h-8 w-full">
                                Ja
                            </button>
                        </form>
                    </div>

                    <div class="w-1/2 text-center">
                        <button class="bg-red-500 rounded-lg max-w-36 h-8 w-full" @click="toggleConfirmDelete()">
                            Nej
                        </button>
                    </div>
                </div>
            </div>

        </div>
        <div v-else>
            <form @submit.prevent="submit">
                <CreateFile v-model="form.file" :input-id-append="inputIdAppend" />
                <div v-if="form.file">{{ submit() }}</div>
            </form>
        </div>

        <InputError v-if="showError" class="mt-2" :message="errorMessage" />
        <UpdateSuccess :recently-successful="showSuccess" class="mt-2">
            {{ successMessage }}
        </UpdateSuccess>
    </div>
</template>
driesvints commented 3 months ago

Hey there,

Can you first please try one of the support channels below? If you can actually identify this as a bug, feel free to open up a new issue with a link to the original one and we'll gladly help you out.

Thanks!