简体   繁体   中英

How can I create a reusable form component for each resource I create and/or update (Vue 3, vue-router, Pinia)

I have a Vue 3 app using Pinia stores that CRUD's data from my rest API. I've just started working with Vue 3 (from smaller vue 2 projects) and this is my first time using Pinia, so I'm still learning the intricacies of both.

One resource I manage from my api is called Applications , and I have a composable that manages API calls to retrive all apps, 1 app, or update the selected app. Instead of creating a form component to UPDATE , and a form component to CREATE applications, I'd like to create a single form component that handles both. So far I can populate my form with an existing application using a route that contains an application_id , and I create a new application if no application_id is in my route.params . I'm just not sure how to tell the form "Hey lets update this application instead of creating it.". I thought of using v-if directives that each create a <button> (one to run update, one to run create method) depending on there is an application_id in my route.params , but that seems inefficient (it may be correct, I'm just lacking knowledge). Here's my code:

// ApplicationStore.js  (pinia store)
import { defineStore } from "pinia";
// Composable for axios API calls
import { getApplications, getApplicationByID, createApplication } from "@/composables/applications";

export const useApplicationStore = defineStore("application", {
  state: () => ({
    applications: [], //list of applications from database
    application: {},  //currently selected application for edit form
    loading: false,
    success: "Successfully Created",
    error: "",
  }),
  getters: {},
  actions: {
    async fetchApplications() {
      this.loading = true;
      this.applications = [];
      const { applications, error } = await getApplications();
      
      this.applications = applications;
      this.error = error;      
      this.loading = false;
    },
    async fetchApplicationByID(id) {
      this.loading = true;
      const { application, error } =  await getApplicationByID(id);
            
      this.application = application;
      this.error = error;
      this.loading = false;
    },
    async createNewApplication() {
      this.loading = true;
      const { application, results, error } = await createApplication(this.application);
      this.application = application;
      this.error = error;
      this.loading = false;

      if (results.status === 201) {
        // show this.success toast message
      }
    }
  }
});

Here is my ApplicationForm component. It currently looks for route.param.id to see if an application is selected, if so it populates the form:

// ApplicationForm.vue
<template>
  <section class="columns">
    <div class="column">
      <div v-if="error" class="notification is-danger">{{ error }}</div>
      <div class="field">
        <label class="label">Name</label>
        <input v-model="application.name" class="input" type="text" />
      </div>
      <div class="field">
        <label class="label">Location</label>
        <input v-model="application.location" class="input" type="text" />
      </div>
      <div class="control">
        <button @click="createNewApplication" class="button">Save</button>
      </div>
    </div>
  </section>
</template>

<script setup>
  import { useRoute } from "vue-router";
  import { useApplicationStore } from "@/stores/ApplicationStore";
  import { storeToRefs } from "pinia";

  const route = useRoute();
  const { applications, application, error } = storeToRefs(useApplicationStore());
  const { createNewApplication } = useApplicationStore();
  
  //checking if there's an id parameter, if so it finds the application from the list in the store
  if (route.params.id) {
    application.value = applications.value.find(app => app.id === Number(route.params.id));
  } else {
    //form is blank
    application.value = {};
    error.value = "";
  }
</script>

Is there a preferred way to use this single form for both create and updates? I wonder if slots would be a good use case for this? But then I think I'd still end up making multiple form components for each CRUD operation. Also, I considered using a v-if to render the buttons based on if an application is in the store or not, like this:

<button v-if="route.params.id" @click="updateApplication" class="button">Update</button>
<button v-else @click="createNewApplication" class="button">Save</button>

I can't help but feel there is a better way to handle this (it is something I'll utilize a lot in this and future projects). This is my first big vue/pinia app. I'm loving the stack so far but these little things make me question whether or not I'm doing this efficiently.

If the form's UI is mainly expected to stay the same except for a few small differences (eg the button text), you could make the form emit a custom "submit" event and then handle that event from the parent component where you render the form (ie on the update page you have <ApplicationForm @submit="updateApplication"> and on the create page you have <ApplicationForm @submit="createNewApplication" /> :

// ApplicationForm.vue
<template>
  <section class="columns">
    <div class="column">
      <div v-if="error" class="notification is-danger">{{ error }}</div>
      <div class="field">
        <label class="label">Name</label>
        <input v-model="application.name" class="input" type="text" />
      </div>
      <div class="field">
        <label class="label">Location</label>
        <input v-model="application.location" class="input" type="text" />
      </div>
      <div class="control">
        <button @click="$emit('submit')" class="button">{{ buttonText }}</button>
      </div>
    </div>
  </section>
</template>

As for the text, you can pass that as a prop (eg buttonText ) to the ApplicationForm component. If some sections of the form are more substantially different than just different text between the "Update" and "Create" form, that's when you'd use slots.

I wouldn't recommend making the <ApplicationForm /> component responsible for reading the route parameters; that should generally be done only by the Vue component responsible for rendering the page (and then it should pass that data through props so that the component is as re-usable as possible)

So your parent component could look something like this:

<ApplicationForm v-if="application" @submit="updateApplication" />
<ApplicationForm v-else @submit="createNewApplication" />

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM