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 >
<div >
<div v-if="error" >{{ error }}</div>
<div >
<label >Name</label>
<input v-model="application.name" type="text" />
</div>
<div >
<label >Location</label>
<input v-model="application.location" type="text" />
</div>
<div >
<button @click="createNewApplication" >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" >Update</button>
<button v-else @click="createNewApplication" >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.
CodePudding user response:
If the form's UI is mainly expected to stay the same except for a few small differences (e.g. 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 (i.e. on the update page you have <ApplicationForm @submit="updateApplication">
and on the create page you have <ApplicationForm @submit="createNewApplication" />
:
// ApplicationForm.vue
<template>
<section >
<div >
<div v-if="error" >{{ error }}</div>
<div >
<label >Name</label>
<input v-model="application.name" type="text" />
</div>
<div >
<label >Location</label>
<input v-model="application.location" type="text" />
</div>
<div >
<button @click="$emit('submit')" >{{ buttonText }}</button>
</div>
</div>
</section>
</template>
As for the text, you can pass that as a prop (e.g. 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" />