inertiajs / inertia

Inertia.js lets you quickly build modern single-page React, Vue and Svelte apps using classic server-side routing and controllers.
https://inertiajs.com
MIT License
6.5k stars 436 forks source link

Inertia link does not reload the page form data #170

Closed tanthammar closed 4 years ago

tanthammar commented 4 years ago

If I am on an the edit page for Organizer A and click on an inertia link to edit Organizer B. Inertia replaces the url but the form is not reloaded and keeps Organizer A data (and vue blows up) (If I click on the link from any other page it works as expected.)

Example href: https://test-site.test/app/organizers/org-YSMqnF-L8VSq8s5jzZ-nB/edit

<inertia-link :href="app.organizers.edit " method="get">{{organizer.name}}</inertia-link>

OrganizerController@edit

public function edit(Organizer $organizer)
{
    return inertia('Organizers/Edit', [
        'organizer' => $organizer,
    ]);
}
rodrigopedra commented 4 years ago

Hi @tanthammar if you can please share some of your page component, at least props and the top level form or the container element that wraps the form.

In the meanwhile one thing you can try is adding a ':key prop to your form element to force Vue re-render the form if some props changes:

<form :key="organizer.id" ...>
... 
</form>

Other thing to check: you might be copying the organizer fields to an object in the component's data() so you can use v-model in your form.

As the page component is kept the same the component won't run the data() when just the prop changed. So you can use the updated() life-cycle hook to update the form fields, if that is the case.

For example:

<script>
export default {
  props: ['organizer'],

  data() {
    return {
      name: this.organizer.name,
      address: this.organizer.address,
    };
  },

  updated() {
    this.name = this.organizer.name;
    this.address = this.organizer.address;
  },
}
</script>

If you have more props that can change from the server prefer using a watcher over using the updated() lifecycle hook, otherwise you user can loose edits:

<script>
export default {
  props: ['organizer'],

  data() {
    return {
      name: this.organizer.name,
      address: this.organizer.address,
    };
  },

  watch {
    organizer(current, old) {
      if (current.id === old.id) return; // same organizer -> ignore

      // different one -> update form fields
      this.name = current.name;
      this.address = current.address;
    },
  },
}
</script>

Hope any of that helps.

EDIT updated watcher code

rodrigopedra commented 4 years ago

Just to add this is a common situation as Vue tries to reuse the components if only the props are changed. As in a form we generally copy the props to local state this can be out of sync.

Vue router even has some docs on how to workaround this:

https://router.vuejs.org/guide/essentials/dynamic-matching.html#reacting-to-params-changes

The solution I like the most is using the :key special prop so all the component gets re-rendered. One issue with that is that if your form is in your top level page component, you should use it on the page component itself which would require tweaking the App render function.

One solution I use for keeping the inertia's page component without the :key but use the :key to reload a form, is to move the form to a component of its own, and add the :key on its parent component.

For example, in your Organizers/Edit.vue component you would have:

<template>
  <OrganizerForm :key="organizer.id" :organizer="organizer" />
</template>

<script>
export default {
  props: ['organizer'],
};
</script>

And in this new OrganizerForm component you would place the form's code. (For code brevity I am assuming the OrganizerForm component is imported globally)

When the organizer's id is changed, Vue will re-render the OrganizerForm component so re-executing any logic on its data().

This way, you would not have to deal with watchers and life-cycle hook and could rely on Vue to keep track of your form.

Another advantage of this approach is that you could reuse this OrganizerForm component for both Organizers/Create and Organizers/Edit pages.

tanthammar commented 4 years ago

Thank you for your fast replies!! I will read them through and test your suggestions.

But before that, I noticed in vue dev tools that Inertia does not update the url:prop nor is the "organizer" prop populated with new data from the backend controller.

This makes me wonder if that there is more to it? Shouldn't Inertia update the page props?

tanthammar commented 4 years ago

Now I tried to add :key to all elements where applicable. Problem remains. Simplified structure is

Organizer/Edit.vue has 14 form components, they are all inside "tabs" Example:

<q-tab-panel name="invoicing">
            <invoicing
              v-if="tab =='invoicing'"
              :post="organizer"
              :uuid="organizer.uuid"
              :routeName="routeName"
              :key="organizer.uuid"
            />
          </q-tab-panel>

Components are lazy-loaded Example

components: {
Invoicing: () => import("@/Shared/Forms/Invoicing"),
}

I also added the key prop to the form tag Example in Invoicing.vue

<standard-form :key="uuid" dusk="invoicing-detailed-form" :fields="json" :uuid="uuid" :routeName="routeName" action="invoicing" />

How about Inertia page props. Should they not be updated when the url changes?

rodrigopedra commented 4 years ago

Out of ideas why. The only thing that cross my mind is to be something on Laravel side.

When making the request from Vue are you using a partial reload?

https://inertiajs.com/requests#partial-reloads

Can you check the response on your browser's console to check if the server is sending the data you expect?

tanthammar commented 4 years ago

The server is sending the correct data and I am not using partial reloads.

tanthammar commented 4 years ago

But BIG BIG thank you for all your efforts.

rodrigopedra commented 4 years ago

You're welcome =) Hope someone has some other idea to help you find the solution to this issue.

Juhlinus commented 4 years ago

Hi @tanthammar !

Have you had a look at the PingCRM example?

I tried adding a link that links to the next organization's edit page, and it works fine.

Could it be that you're not using the model property to make the data responsive?

From PingCRM:

<template>
      <form @submit.prevent="submit">
        <div class="p-8 -mr-6 -mb-8 flex flex-wrap">
          <text-input v-model="form.name" :errors="$page.errors.name" class="pr-6 pb-8 w-full lg:w-1/2" label="Name" />
          <text-input v-model="form.email" :errors="$page.errors.email" class="pr-6 pb-8 w-full lg:w-1/2" label="Email" />
          <text-input v-model="form.phone" :errors="$page.errors.phone" class="pr-6 pb-8 w-full lg:w-1/2" label="Phone" />
          <text-input v-model="form.address" :errors="$page.errors.address" class="pr-6 pb-8 w-full lg:w-1/2" label="Address" />
          <text-input v-model="form.city" :errors="$page.errors.city" class="pr-6 pb-8 w-full lg:w-1/2" label="City" />
          <text-input v-model="form.region" :errors="$page.errors.region" class="pr-6 pb-8 w-full lg:w-1/2" label="Province/State" />
          <select-input v-model="form.country" :errors="$page.errors.country" class="pr-6 pb-8 w-full lg:w-1/2" label="Country">
            <option :value="null" />
            <option value="CA">Canada</option>
            <option value="US">United States</option>
          </select-input>
          <text-input v-model="form.postal_code" :errors="$page.errors.postal_code" class="pr-6 pb-8 w-full lg:w-1/2" label="Postal code" />
        </div>
        <div class="px-8 py-4 bg-gray-100 border-t border-gray-200 flex items-center">
          <button v-if="!organization.deleted_at" class="text-red-600 hover:underline" tabindex="-1" type="button" @click="destroy">Delete Organization</button>
          <loading-button :loading="sending" class="btn-indigo ml-auto" type="submit">Update Organization</loading-button>
        </div>
      </form>
      [...]
</template>

<script>
export default {
  props: {
    organization: Object,
  },
  data() {
    return {
      sending: false,
      form: {
        name: this.organization.name,
        email: this.organization.email,
        phone: this.organization.phone,
        address: this.organization.address,
        city: this.organization.city,
        region: this.organization.region,
        country: this.organization.country,
        postal_code: this.organization.postal_code,
      },
    }
  },
  [...]
}
</script>
tanthammar commented 4 years ago

In Edit.vue I receive the page props

  props: {
    organizer: {
      type: [Object, Array]
    },

I pass it down to the component that defines the form fields (there are 14 form components...)

<q-tab-panel name="invoicing">
            <invoicing
              v-if="tab =='invoicing'"
              :post="organizer"
              :uuid="organizer.uuid"
              :routeName="routeName"
              :key="organizer.uuid"
            />
          </q-tab-panel>

The field components are lazy-loaded Maybe this is the problem ??

components: {
Invoicing: () => import("@/Shared/Forms/Invoicing"),
}

In Invoicing.vue I use the organizer prop (renamed to post) to populate form data

props: {
    post: {
      type: [Object, Array]
    },

A field definition example

data() {
    return {
      fields: {
      business_no: {
          name: "business_no",
          value: this.post.business_no,
          label: this.$trans("fields.business_no"),
          hint: this.$trans("fields.business_no_hint"),
          type: "text"
        },

The field definition component passes the fields as props to a form builder component The organizer prop is not passed to the form component. (Could this be the problem?)? The form only receives initial field values, a route and a model uuid,

<standard-form dusk="invoicing-basic-form" :fields="fields" :uuid="uuid" :routeName="routeName" action="invoice_cols" />

StandardForm.vue

  props: {
    fields: {
      type: [Object, Array],
      required: true,
    }
  },
  data() {
    return {
      form: this.fields,
    }
  },

Simplified form

<template>
  <form>
    <div v-for="(field, index) in form" :key="index">
          <input v-model="field.value" :name="field.name" />
    </div>
    <button label="save" type="submit" color="primary" />
  <form>
</template>
tanthammar commented 4 years ago

I did another test today. Instead of passing down the "organizer" prop from the page through all child components I tied it to $page. And It almost worked ...

Before:

|-Edit.vue, props: "organizer" = $page.organizer
   |-<invoicing :post=organizer />

After

|-Edit.vue, props: "organizer" = $page.organizer
    |-<invoicing /> props: "post" = $page.organizer

The initial v-model data, in the form fields are updated.

But I got errors in console. There is something going on with Inertia destroying components. The console is full with errors from components not being able to destroy their listeners on Vue beforeDestroy hook.

I guess that is another topic.

reinink commented 4 years ago

Hey @tanthammar!

We're trying to clean up some old issues. Do you know if this problem is still relevant?

IvanBernatovic commented 4 years ago

@reinink I'm running into a similar issue when I submit the PUT request and redirect back to a form. Maybe I'm doing something wrong here but here's the code and steps for setup and reproducing the issue. https://github.com/IvanBernatovic/inertia-vue-issue I reuse forms by extracting them into a separate component, that way I use the same component for create and edit form. If I put :key="Math.random().toString(36)" on the form component, that fixes the issue. Btw. I'm using Laravel Jetstream app in the example but it shouldn't matter.

reinink commented 4 years ago

@IvanBernatovic So, this is actually being caused by Jetstream, not Inertia.js. It's because Jetstream defaults form submission to { resetOnSuccess: true }, which is actually problematic if you submit a form back to the same page. Basically what happens is Jetstream captures the props on the initial component load and saves them in local memory. Then, when you make a request to submit the form, it returns back to the same component, which still has those original values in memory, and Jetstream restores those values.

Update your UserForm.vue file to fix this:

form: this.$inertia.form({
  name: this.user?.name || "",
  email: this.user?.email || "",
}, {
  resetOnSuccess: false,
}),

IMHO, Jetstream shouldn't default resetOnSuccess to true, but rather this should be an opt-in thing you use when it's needed (ie. resetting password inputs back to blank).

reinink commented 4 years ago

So, I am going to close this issue, because I actually think I remember what was wrong here, and it's been fixed.

The issue had to do with component state when making GET requests to the same component. Ping CRM actually suffered from this bug originally as well. This was fixed by introducing the preserveState feature.

My only concern is that this issue was posted long after that feature was added. The only thing I can think is that @tanthammar maybe was using an older version of Inertia.

If someone is able to provide a repo that reproduces this issue, I'd be more than happy to have another look at this. 👍

IvanBernatovic commented 4 years ago

@reinink Thanks for the quick response. That was it so thank you very much, and it was Jetstream after all.