inertiajs / inertia-laravel

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

Inertia redirects to / after failed validation #520

Closed NicolasSacC closed 10 months ago

NicolasSacC commented 1 year ago

Hi!

I have this weird issue: Inertia constantly redirects to / after a failed validation.

If a form validation fails, Inertia returns a response with the parameter url as "/".

However, there is one component where it doesn't happen. "Pages/Dashboard"

Any other component has that issue.

See a comparison here (first is ok, second redirects to /):

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import {Head, useForm} from '@inertiajs/vue3';
import PageHeader from "@/Components/PageHeader.vue";
import PrimaryButton from "@/Components/PrimaryButton.vue";
import { ref } from 'vue';
import SecondaryButton from "@/Components/SecondaryButton.vue";
import Modal from "@/Components/Modal.vue";
import TextInput from "@/Components/TextInput.vue";
import InputLabel from "@/Components/InputLabel.vue";
import InputError from "@/Components/InputError.vue";
import {Link} from "@inertiajs/vue3";
import LinkPrimaryButton from "@/Components/LinkPrimaryButton.vue";

const creatingNewSite = ref(null);
const nameInput = ref(null);

const form = useForm({
    name: '',
});

const createNewSiteForm = () => {
    creatingNewSite.value = true;
}

const createSite = () => {
    form.post(route('websites.store'), {
        onError: () => nameInput.value.focus(),
    });
};

const closeModal = () => {
    creatingNewSite.value = false;

    form.reset();
};

</script>

<template>
    <Head title="Tableau de bord" />

    <AuthenticatedLayout>
        <template #header>
            <PageHeader>Tableau de bord</PageHeader>
            <PrimaryButton @click="createNewSiteForm" v-if="$page.props.websites.length != 0">Créer un nouveau site</PrimaryButton>
        </template>

      <div class="space-y-6">
        <article v-for="website in $page.props.websites" class="bg-white flex flex-col lg:flex-row items-center lg:items-start justify-between rounded-lg shadow-md p-4 lg:p-6">
          <div class="w-full lg:w-1/2 flex justify-center lg:justify-start">
            <div class="relative">
              <img src="https://images.unsplash.com/photo-1496128858413-b36217c2ce36?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3603&q=80" alt="" class="aspect-[16/9] w-full rounded-2xl bg-gray-100 object-cover sm:aspect-[2/1] lg:aspect-[3/2]">
              <div class="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10"></div>
            </div>
          </div>
          <div class="w-full lg:w-1/2 max-w-xl mt-4 lg:mt-0 lg:pl-6 flex flex-col justify-center lg:justify-between">
            <div class="flex flex-col justify-center h-full">
              <div class="flex items-center gap-x-4 text-xs">
                <time datetime="2020-03-16" class="text-gray-500">Dernière modification le 16 mars 2023</time>
              </div>

              <div class="group relative mt-2">
                <h3 class="text-lg font-semibold leading-6 text-gray-900 group-hover:text-gray-600">
                  <Link :href="route('websites.show', website.slug)">
                    <span class="absolute inset-0"></span>
                    {{ website.name }}
                  </Link>
                </h3>
              </div>
              <div class="mt-4 lg:mt-0 lg:pt-2 flex justify-center lg:justify-start">
                <LinkPrimaryButton class="btn-small" :href="route('websites.show', website.slug)">Modifier le site</LinkPrimaryButton>
              </div>
            </div>
          </div>
        </article>
        <div class="text-center" v-if="$page.props.websites.length == 0">
          <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
              <path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 013 12c0-1.605.42-3.113 1.157-4.418" />
          </svg>
          <h3 class="mt-2 text-sm font-semibold text-gray-900">Aucun site web</h3>
          <p class="mt-1 text-sm text-gray-500">Créez un nouveau site web dès maintenant!</p>
          <div class="mt-4">
            <PrimaryButton class="btn-small" @click="createNewSiteForm">
              <svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
                <path d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" />
              </svg>
              Créer un nouveau site web
            </PrimaryButton>
          </div>
        </div>

      </div>

        <Modal :show="creatingNewSite" @close="closeModal">
            <div class="p-6">
                <h2 class="text-lg font-medium text-gray-900">
                    Create site
                </h2>

                <div class="mt-6">
                    <InputLabel for="name" value="Site name" />

                    <TextInput
                        id="name"
                        ref="nameInput"
                        v-model="form.name"
                        type="name"
                        class="mt-1 block w-3/4"
                        @keyup.enter="createSite"
                    />

                    <InputError :message="form.errors.name" class="mt-2" />
                </div>

                <div class="mt-6 flex justify-end">
                    <SecondaryButton @click="closeModal"> Cancel </SecondaryButton>

                    <PrimaryButton
                        class="ml-3"
                        :class="{ 'opacity-25': form.processing }"
                        :disabled="form.processing"
                        @click="createSite"
                    >
                        Create site
                    </PrimaryButton>
                </div>
            </div>
        </Modal>

    </AuthenticatedLayout>
</template>
    public function store(Request $request) {
        $data = $request->validate([
            'name' => 'required|string|min:1|max:255'
        ]);

        $website = $request->user()->websites()->create($data);

        $page = $website->pages()->create([
            'name' => 'Accueil'
        ]);

        $website->homepage_id = $page->id;
        $website->save();

        return to_route('websites.show', $website);
    }
<script setup>
import PrimaryButton from "@/Components/PrimaryButton.vue";
import Modal from "@/Components/Modal.vue";
import {useForm, usePage} from "@inertiajs/vue3";
import {ref} from "vue";
import SecondaryButton from "@/Components/SecondaryButton.vue";
import InputError from "@/Components/InputError.vue";
import InputLabel from "@/Components/InputLabel.vue";
import TextInput from "@/Components/TextInput.vue";
import ColorThief from "colorthief";

const {model} = defineProps(['model']);
const creatingEvent = ref(false);
const form = useForm({
  name: '',
  description: '',
  image: '',
  place: '',
  starts_at: '',
  ends_at: '',
  inscription_link: ''
})

const fileInput = ref(null);
const currentImage = ref(null);

function onSelectImage($event) {

  let file = $event.target.files[0];
  form.image = file;

  if (!file) {
    form.image = '';
    return;
  }

  const reader = new FileReader();
  reader.onload = (e) => {
    currentImage.value = e.target.result;
  };

  reader.readAsDataURL(file);
}

function closeModal() {
  form.reset();
  if(fileInput.value != null) {
    fileInput.value.value = null;
  }

  currentImage.value = null;

  creatingEvent.value = false;
}

const store = () => {
  form.post(route('events.store', usePage().props.website.slug), {
    onError: () => {},
    onSuccess: () => {
      closeModal();
    }
  })
}

</script>

<template>
  <PrimaryButton @click="creatingEvent = true;">Créer un évènement</PrimaryButton>
  <Modal :show="creatingEvent" @close="closeModal">
    <div class="p-6">
      <h2 class="text-lg font-medium text-gray-900">
        Créer un évènement
      </h2>

      <form @submit.prevent="store">
        <img ref="image" :src="currentImage" v-if="currentImage != null" class="max-h-32 mt-6"/>

        <div class="mt-6" key="logo">
          <InputLabel for="image" value="Image"/>
          <input ref="fileInput" type="file" @input="onSelectImage" />
          <InputError :message="form.errors.image" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="name" value="Nom de l'évènement" />

          <TextInput
              id="name"
              v-model="form.name"
              type="text"
              class="mt-1 block w-3/4"
              placeholder="Nom de l'évènement"
          />

          <InputError :message="form.errors.name" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="description" value="Description" />

          <textarea
              id="description"
              v-model="form.description"
              type="text"
              class="textarea mt-1 block w-3/4"
              placeholder="Description"
          />

          <InputError :message="form.errors.description" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="place" value="Lieu" />

          <TextInput
              id="place"
              v-model="form.place"
              type="text"
              class="textarea mt-1 block w-3/4"
              placeholder="Lieu"
          />

          <InputError :message="form.errors.place" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="starts_at" value="Débute le" />

          <input
              id="starts_at"
              v-model="form.starts_at"
              type="datetime-local"
              class="input mt-1 block w-3/4"
              placeholder="Débute le "
          />

          <InputError :message="form.errors.starts_at" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="ends_at" value="Termine le" />

          <input
              id="ends_at"
              v-model="form.ends_at"
              type="datetime-local"
              class="input mt-1 block w-3/4"
              placeholder="Termine le "
          />

          <InputError :message="form.errors.ends_at" class="mt-2" />
        </div>

        <div class="mt-6">
          <InputLabel for="inscription_link" value="Lien d'inscription" />

          <TextInput
              id="inscription_link"
              v-model="form.inscription_link"
              class="input mt-1 block w-3/4"
              placeholder="Lien d'inscription"
          />

          <InputError :message="form.errors.inscription_link" class="mt-2" />
        </div>

        <div class="mt-6 flex justify-end">
          <SecondaryButton @click="closeModal"> Annuler </SecondaryButton>

          <PrimaryButton
              class="ml-3"
              :class="{ 'opacity-25': form.processing }"
              :disabled="form.processing"
              @click="store"
          >
            Créer un évènement
          </PrimaryButton>
        </div>

      </form>
    </div>
  </Modal>

</template>
    public function store(Website $website, Request $request) {
        $data = $request->validate([
            'name' => 'required|string|min:1|max:255',
            'description' => 'required|string|min:1|max:10000',
            'place' => 'required|string|min:1|max:255',
            'starts_at' => 'required|date|before:ends_at',
            'ends_at' => 'required|date|after:starts_at',
            'inscription_link' => 'required|url|max:255',
            'image' => 'nullable|image|max:5120'
        ]);

        $event = $website->events()->create(Arr::except($data, 'image'));

        $event->uploadImage('image', $request->file('image'), 600);

        $event->save();

        return to_route('events.index', $website);
    }
NicolasSacC commented 1 year ago

I found that it was because url()->previous() after the validation fails returns the homepage for some reason, but I have no idea why...

jameshulse commented 1 year ago

@NicolasSacC, see: https://inertiajs.com/redirects

ravibpatel commented 1 year ago

This happens cause when using Inertia, previous URL is not stored due to AJAX requests used by Inertia to facilitate the SPA functionality. You can see why it is the case here. It can be solved if Laravel can consider Inertia requests as valid request to store as current URL. In that case, changing it to the following works.

protected function storeCurrentUrl(Request $request, $session)
{
    if ($request->isMethod('GET') &&
        $request->route() instanceof Route &&
        ! ($request->ajax() && ! $request->inertia()) &&
        ! $request->prefetch() &&
        ! $request->isPrecognitive()) {
        $session->setPreviousUrl($request->fullUrl());
    }
}

For now, you can create the FormRequest instead of defining the validation logic in controller and inside the FormRequest you can assign $redirectRoute property to route where you want to redirect in case of failed validation.

You can also create your own StartInertiaSession middleware and extend the original Middleware provided by Laravel. Just override the StoreCurrentUrl method as shown above and change the \Illuminate\Session\Middleware\StartSession::class, to \App\Http\Middleware\StartInertiaSession::class in $middlewareGroups array of app\Http\Kernel.php and it should work too. You can see the modified middleware here.

Although, I still think Inertia should update the session key for previous URL internally. I think @reinink should look at this because it will redirect to the page where you navigated first using normal request in case of failed validation. That causes very abrupt behavior and create a confusion for devs using the Inertia.

reinink commented 10 months ago

I found that it was because url()->previous() after the validation fails returns the homepage for some reason, but I have no idea why...

Glad you got it figured out! 👍

Tdiouf10 commented 10 months ago

I had this problem today, I found it and solved it, it's an inertia import problem

  1. First step : You will uninstall: @inertiajs/vue3 npm uninstall @inertiajs/vue3 or yarn remove @inertiajs/vue3

  2. Second step : You will install : @inertiajs/inertia-vue3 npm install @inertiajs/inertia-vue3 or yarn add @inertiajs/inertia-vue3

  3. Third step : go to your app.js file in ressources/js Replace import { createInertiaApp } from '@inertiajs/vue3' by import { createInertiaApp } from '@inertiajs/inertia-vue3'

  4. Last step : Replace everywhere import { createInertiaApp } from '@inertiajs/vue3' by import { createInertiaApp } from '@inertiajs/inertia-vue3'

Delete the nodes_modules folder and do an : npm install or yarn install

Hope this helps you if you encounter this problem 😉