การ Build Full-stack Applications ด้วย Scalability ในใจ intimidating (ทำให้เฉยเมย/กลัว) โดยเฉพาะ Build ด้วย Latest Version ของ Vue และ Vuex ที่มี Full TypeScript Support บทความนี้จะสอนทุกอย่างที่ Readers ต้องรู้เกี่ยวกับ Building Scalable Full-stack Applications จาก State Management ด้วย Vuex 4.0 จัดการ API Requests และ Database Interactions โดยสำรวจ CRUD Application ที่ Manage Prescription ของ Treatments ให้ Unhealthy Livestock Backend สร้างด้วย Rails Expose Basic CRUD APIs สำหรับ Integration โดย Frontend
Most Companies Adopted API-based Development Architecture เนื่องจากให้ Frontend Team Flexibility เลือก Frontend Tool ที่ Suitable แม้ไม่ใช่ Best Option แต่มัน Reliable Option และหลาย Companies Adopted Development Architecture นี้เพราะเพิ่ม Flexibility ให้ Team
Prerequisites
ก่อนเริ่ม ต้องมีสิ่งต่อไปนี้เพื่อ Keep Up กับ Tutorial:
- Rails V6.x
- Node.js V10.x
- Prior Working Knowledge of Ruby on Rails
- Prior Working Knowledge of TypeScript
- Prior Working Knowledge of Vue.js
What we will build

ใน Tutorial นี้ เราจะ Build Full-stack CRUD Application Manage Prescription of Treatments สำหรับ Unhealthy Livestock Users สามารถ Create, Update, และ Delete Prescriptions สำหรับ Livestock CRUD APIs สร้างด้วย Rails Expose CRUD APIs สำหรับ Integration โดย Frontend สร้างด้วย Vue 3 แต่ละ Stack รันบน Different Ports เป็น Independent Applications
Basic Architecture of the Full-stack App
Full-stack Application ของเราประกอบด้วย Client และ Server Applications รัน Independently แต่ละ Component ของ Client Application Interact กับ CRUD APIs ที่ Server Issue ผ่าน Vuex สำหรับ Proper Management ของ Application State Backend Application Store Prescription Data ทั้งหมดใน Sqlite 3 Database ขณะ Expose CRUD APIs ให้ Frontend Application
Backend Service Setup
ใน Folder ที่ชอบ สร้าง Rails App ด้วย Command:
rails new vet_clinic_api --api Flag --api บอก Rails สร้าง Project as an API ทำให้ Remove Frontend Dependencies (View Files)
Database Configuration
เราจะใช้ Sqlite 3 Default Database สำหรับ Rails Applications
rails g scaffold prescriptions vet_prescription:text prescribed_by:text disease:text livestock:text completed:boolean Run Command นี้ Rails Scaffold Initial Structure สำหรับ Migration, Test, Model, Controller, Route:
rails db:migrate Command นี้ Add Tables เข้า Database
Seeding the Database
Seed Database ด้วย Prescription Data เพิ่ม Code Snippet นี้ใน db/migrate/seed.rb
//db/migrate/seed.rb Prescription.destroy_all Prescription.create!([{ vet_prescription:"Achyranthes aspera", prescribed_by:"Dr Chucks", disease:"Rabbies", livestock:"goat", completed:false }, { vet_prescription:"Achyranthes aspera", prescribed_by:"Dr Rex", disease:"Rabbies", livestock:"Dog", completed:false }, { vet_prescription:"ethnovet", prescribed_by:"Dr Chucks", disease:"Pox", livestock:"Sheep", completed:false }]) p "Created #{Prescription.count} prescriptions" File นี้ Store Initial Data สำหรับ Seed Database เมื่อ App Start Up เราจะมี Existing Prescription Data
Run Command Execute Code ใน db/migrate/seed.rb Seed Database:
rails db:seed With These Few Commands เราสร้าง Functional CRUD API ด้วย Rails Easy มากไหม? (Smiles)
CORS Configuration
เนื่องจากเรา Expose CRUD APIs ให้ Frontend และ Frontend/Backend Servers รัน Different PORT ต้อง Setup CORS Configuration บน Rails Backend Grant Access Data Sharing ระหว่าง Frontend และ Backend
Locate Gemfile ใน Project Root Uncomment Line:
# gem 'rack-cors' Add Code ใน config/environments/initializers/cors.rb:
//config/environments/initializers/cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end Snippet นี้ Allow Rails CRUD API Accessible จาก Frontend Apps รัน Any PORT
Run Command Start Rails Server:
rails s Navigate localhost:3000/prescriptions ได้ JSON Response สำหรับ All Prescriptions
Consider Using Rails ถ้าทำงานกับ Clients ที่ Change Project Workflows บ่อย ด้วย Rails Implement Features ด้วย Few Commands และ Lines of Code นี่ Personal Opinion
Introducing Vue
Vue เป็น Progressive Framework สำหรับ Building User Interfaces Vue Introduce Concepts เช่น Virtual DOM, Render Functions, และ Server-side Rendering Capabilities ให้ Significant Performance Optimization สำหรับ Web Applications
Vue 3 มาพร้อม New Features และ Changes มากสำหรับ Developers Features นี้ Designed เพื่อ Improve Overall Stability, Speed และ Maintainability
เราจะใช้ Composition API หนึ่งใน Most Anticipated Features ของ Vue 3 มัน New Way Create Vue Components ด้วย Organized และ Efficient Approach เขียน Code และ Full TypeScript Type-checking Support
Introducing Vuex
Vuex เป็น State Management Library สร้างโดย Vue Team Based on Same Flux Architecture as Redux Designed Specifically สำหรับ Vue Allow Better Organization ของ Store Vuex จะ Crucial ถ้า Vue Application State Gets More Complex As It Grows Latest Stable Release Vuex v4.0.0 Support Composition API ใน Vue 3 และ Robust Inference สำหรับ TypeScript
Setting up the Frontend Application
Frontend Setup ด้วย Vue 3 และ TypeScript ในขณะที่ Vuex ใช้สำหรับ Application State Management
Start by Using Vue-CLI Tool Create Vue 3 App With TypeScript Support
Install Vue-CLI Tool Globally:
npm install --global @vue/cli Create New Vue 3 App With TypeScript และ Vuex Support:
vue create vet_clinic_frontend Choose Manually Select Feature Option Hit Space Key Select Options:
- Choose Vue Version
- Babel
- TypeScript
- Linter / Formatter
Next Choose Vue 3.x(Preview) as Version for Project
- Enter Yes Use Class-style Component Syntax
- Enter Yes Use Babel Alongside TypeScript
- Select Any Linter of Choice
Once Vue 3 App Generated Successfully เราจะมี Vue 3 Project Setup พร้อม Vuex (V4.x) และ Full TypeScript Support
เราจะ Add Type Safety ด้วย TypeScript Start Dev Server Run Command ใน Terminal ไป https://localhost:8080 Preview Project in Browser
เราจะใช้ Bulma CSS Framework สำหรับ Styling App Run Command Install Bulma CSS:
npm install bulma Import Bulma CSS Add Code ใน App.vue:
//App.vue <style lang="scss"> @import "~bulma/css/bulma.css"; </style> Setting up the Application Store
Application Store Setup ด้วย Vuex To Make Changes to Store Actions Dispatched from Component Triggers Mutations Thereby Updating Store
Take Steps Setup Application Store:
- Create State Objects
- Set Up Mutations That Will Occur
- Create Actions That Commit to Subsequent Mutations
- Create Getters for Components to Directly Compute State Data
State
State คือ Store Object ที่ Hold Application-level Data ที่ต้อง Accessed by All Components
Create state.ts File in Store Directory With Code:
//src/store/state.ts export type Prescription = { id: number; vet_prescription: string; prescribed_by: string; disease: string; livestock: string; completed: boolean; editing: boolean; }; export type Data = { vet_prescription: string; prescribed_by: string; disease: string; livestock: string; }; export type State = { loading: boolean; prescriptions: Prescription[]; data: Data | null; showCreateModal: boolean; showEditModal: boolean; showPrescriptionModal: boolean; editModalPrescriptionId: number | undefined; showPrescriptionId: number | undefined; }; export const state: State = { loading: false, prescriptions: [], data: null, showCreateModal: false, showEditModal: false, showPrescriptionModal: false, editModalPrescriptionId: undefined, showPrescriptionId: undefined, }; ที่นี่เรา Add Type Safety ให้ Prescription และ Data เรา Export Types เพราะจะ Used ใน Definitions ของ Getters, Mutations, และ Actions สุดท้าย Cast State Type ให้ state
Mutations
Mutations คือ Methods ที่เมื่อ Triggered Modify Store พวกมัน Receive State as First Argument และ Payload as Second Eventually Modify Application State with Payload To Create Mutations Vuex Docs Recommend Using Constants for Mutation Types
Create mutations.ts File in Store Directory With Code:
//src/store/mutations.ts import { MutationTree } from "vuex"; import { State, Prescription, Data } from "./state"; export enum MutationType { CreatePrescription = "CREATE_PRESCRIPTION", SetPrescriptions = "SET_PRESCRIPTIONS", CompletePrescription = "COMPLETE_PRESCRIPTION", RemovePrescription = "REMOVE_PRESCRIPTION", EditPrescription = "EDIT_PRESCRIPTION", UpdatePrescription = `UPDATE_PRESCRIPTION`, SetLoading = "SET_LOADING", SetCreateModal = "SET_CREATE_MODAL", SetEditModal = "SET_EDIT_MODAL", SetPrescriptionModal = "SET_PRESCRIPTION_MODAL", } Snippet นี้ใช้ Enum Symbol Hold All Possible Names of Mutations ใน App
Next Declare Contract (Type) สำหรับแต่ละ MutationType:
//src/store/mutation.ts export type Mutations = { [MutationType.CreatePrescription](state: State, prescription: Data): void; [MutationType.SetPrescriptions](state: State, prescription: Prescription[]): void; [MutationType.CompletePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void; [MutationType.RemovePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void; [MutationType.EditPrescription](state: State, prescription: Partial<Prescription> & { id: number }): void; [MutationType.UpdatePrescription](state: State, prescription: Partial<Prescription> & { id: number }): void; [MutationType.SetLoading](state: State, value: boolean): void; [MutationType.SetCreateModal](state: State, value: boolean): void; [MutationType.SetEditModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void; [MutationType.SetPrescriptionModal](state: State, value: { showModal: boolean; prescriptionId: number | undefined }): void; }; Then Implement Contracts Declared สำหรับแต่ละ MutationType:
//src/store/mutation.ts export const mutations: MutationTree<State> & Mutations = { [MutationType.CreatePrescription](state, prescription) { state.data == prescription; }, [MutationType.SetPrescriptions](state, prescriptions) { state.prescriptions = prescriptions; }, [MutationType.CompletePrescription](state, newPrescription) { const prescription = state.prescriptions.findIndex((prescription) => prescription.id === newPrescription.id); if (prescription === -1) return; state.prescriptions[prescription] = { ...state.prescriptions[prescription], ...newPrescription }; }, [MutationType.RemovePrescription](state, Prescription) { const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id); if (prescription === -1) return; //If prescription exist in the state, remove it state.prescriptions.splice(prescription, 1); }, [MutationType.EditPrescription](state, Prescription) { const prescription = state.prescriptions.findIndex((prescription) => prescription.id === Prescription.id); if (prescription === -1) return; //If prescription exist in the state, toggle the editing property state.prescriptions[prescription] = { ...state.prescriptions[prescription], editing: !state.prescriptions[prescription].editing }; console.log("prescription", state.prescriptions[prescription]); }, [MutationType.UpdatePrescription](state, Prescription) { state.prescriptions = state.prescriptions.map((prescription) => { if (prescription.id === Prescription.id) { return { ...prescription, ...Prescription }; } return prescription; }); }, [MutationType.SetLoading](state, value) { state.loading = value; }, [MutationType.SetCreateModal](state, value) { state.showCreateModal = value; }, [MutationType.SetEditModal](state, value) { state.showEditModal = value.showModal; state.editModalPrescriptionId = value.prescriptionId; }, [MutationType.SetPrescriptionModal](state, { showModal, prescriptionId }) { state.showPrescriptionModal = showModal; state.showPrescriptionId = prescriptionId; }, }; MutationTree เป็น Generic Type Shipped with Vuex Package เราใช้ใน Snippet นี้ Declare Type of Mutation Tree Mutation Tree และ Mutations Ensure Contract Implemented Correctly ไม่งั้น Typescript Trigger Error
Actions
Actions คือ Methods ที่ Trigger Mutations When Handling Asynchronous Tasks เช่น Making Request to API Actions Used Before Calling Corresponding Mutations with API Response as Payload เราจะได้ Clear Implementation สำหรับ Scenario นี้ขณะ Create Actions
Before Creating Actions Install Axios Handle All Http Requests to Rails Server:
npm install axios --save Create actions.ts File in Store Directory With Code:
//src/store/actions.ts import { ActionContext, ActionTree } from "vuex"; import { Mutations, MutationType } from "./mutations"; import { State, Prescription, Data } from "./state"; import axios from "axios"; const apiUrl = "https://localhost:3000/prescriptions"; export enum ActionTypes { GetPrescriptions = "GET_PRESCRIPTIONS", SetCreateModal = "SET_CREATE_MODAL", SetEditModal = "SET_EDIT_MODAL", RemovePrescription = "REMOVE_PRESCRIPTION", CreatePrescription = "CREATE_PRESCRIPTION", UpdatePrescription = "UPDATE_PRESCRIPTION", } Similarly Snippet นี้ใช้ Enum Symbol Hold All Possible Names of Actions ใน App
Next Declare Contract (Type) สำหรับแต่ละ ActionType:
//src/store/actions.ts type ActionAugments = Omit<ActionContext<State, State>, "commit"> & { commit<K extends keyof Mutations>(key: K, payload: Parameters<Mutations[K]>[1]): ReturnType<Mutations[K]>; }; export type Actions = { [ActionTypes.GetPrescriptions](context: ActionAugments): void; [ActionTypes.SetCreateModal](context: ActionAugments): void; [ActionTypes.SetEditModal](context: ActionAugments): void; [ActionTypes.RemovePrescription](context: ActionAugments, Prescription: { id: number }): void; [ActionTypes.CreatePrescription](context: ActionAugments, data: Data): void; [ActionTypes.UpdatePrescription](context: ActionAugments, prescription: Prescription): void; }; ActionAugments Type Restricts All Commits Only to Declared Mutations และ Check Payload Type
Next Implement Contract (Types) Declared สำหรับแต่ละ ActionType Add Code Below to action.ts File:
//src/store/actions.ts export const actions: ActionTree<State, State> & Actions = { async [ActionTypes.GetPrescriptions]({ commit }) { commit(MutationType.SetLoading, true); const response = await axios.get(apiUrl); commit(MutationType.SetLoading, false); commit(MutationType.SetPrescriptions, response.data); }, async [ActionTypes.SetCreateModal]({ commit }) { commit(MutationType.SetCreateModal, true); }, async [ActionTypes.SetEditModal]({ commit }) { commit(MutationType.SetEditModal, { showModal: true, prescriptionId: 1 }); }, //Optimistic update async [ActionTypes.RemovePrescription]({ commit }, Prescription) { if (Prescription != undefined) { commit(MutationType.RemovePrescription, Prescription); } const response = await axios.delete(`${apiUrl}/${Prescription.id}`); }, async [ActionTypes.CreatePrescription]({ commit, dispatch }, Prescription) { const response = await axios.post(apiUrl, Prescription); dispatch(ActionTypes.GetPrescriptions); }, async [ActionTypes.UpdatePrescription]({ commit, dispatch }, Prescription) { if (Prescription != undefined) { commit(MutationType.UpdatePrescription, Prescription); const response = await axios.patch(`${apiUrl}/${Prescription.id}`, Prescription); dispatch(ActionTypes.GetPrescriptions); } }, }; ที่นี่เราสร้าง Actions Variable เก็บ All Implemented Actions Similarly ActionTree<State> & Actions Ensures Contract (type Actions) Implemented Correctly ไม่งั้น Typescript Trigger Error
เรา Setup Asynchronous Call to Rails API Endpoint ใน GetPrescriptions Action และ Trigger SetPrescriptions Mutation Type with Response Data as Payload เรายัง Setup SetCreateModal, SetEditModal, CreatePrescription, UpdatePrescription, และ RemovePrescription Actions
Getters
Getters คือ Methods ที่ Receive State as First Parameter และ Return Computed Information from Store State
Create getters.ts File in Store Directory With Code:
//src/store/getters.ts import { GetterTree } from "vuex"; import { State, Prescription } from "./state"; export type Getters = { completedPrescriptionCount(state: State): number; totalPrescriptionCount(state: State): number; getPrescriptionById(state: State): (id: number) => Prescription | undefined; }; export const getters: GetterTree<State, State> & Getters = { completedPrescriptionCount(state) { return state.prescriptions.filter((prescription) => prescription.completed).length; }, totalPrescriptionCount(state) { return state.prescriptions.length; }, getPrescriptionById: (state) => (id: number) => { return state.prescriptions.find((prescription) => prescription.id === id); }, }; Code Snippet นี้ Define Getters ต่อไป:
completedPrescriptionCount– Function Gets Total Number of Completed Prescriptions ใน StatetotalPrescriptionCount– Function Gets Total Number of Prescriptions ใน StategetPrescriptionById– Function Gets Prescription by its ID
เรายัง Add Type Safety ให้ Getters
Store
Wire state, mutations, actions, และ getters เข้า Global Vuex Store Update store/index.ts:
//src/store/index.ts import { createStore, Store as VuexStore, CommitOptions, DispatchOptions, createLogger } from "vuex"; import { State, state } from "./state"; import { Mutations, mutations } from "./mutations"; import { Actions, actions } from "./actions"; import { Getters, getters } from "./getters"; export const store = createStore<State>({ plugins: process.env.NODE_ENV === "development" ? [createLogger()] : [], state, mutations, actions, getters, }); export function useStore() { return store as Store; } export type Store = Omit<VuexStore<State>, "getters" | "commit" | "dispatch"> & { commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(key: K, payload: P, options?: CommitOptions): ReturnType<Mutations[K]>; } & { dispatch<K extends keyof Actions>(key: K, payload?: Parameters<Actions[K]>[1], options?: DispatchOptions): ReturnType<Actions[K]>; } & { getters: { [K in keyof Getters]: ReturnType<Getters[K]>; }; }; state, mutations, actions, และ getters Required to Create Store by Passing Object Containing Them into createStore Method During Development createLogger Plugin Logs State (Previous State and Next State) และ Mutations to Console To Have Store Accessible in All Components เราต้อง Inject It Into Entire Application Fortunately Vue-CLI Tool Already Imported Entire Store และ Passed It Within Application's Vue Instance
Integrating the Vuex Store into the Component
ใน Tutorial นี้ Vue 3 Composition API จะใช้ Create All Components ใน Frontend Application
App Component
Frontend ควร Render List of Prescription Data As Soon As It Launches เราจะ Dispatch GetPrescription Action Within Component's mounted() Lifecycle Hook To Access Store Within Component useStore Hook ซึ่ง Returns Our Store จะ Execute
//src/App.vue <script lang="ts"> import { computed, defineComponent, onMounted } from "vue"; import PrescriptionList from "./components/PrescriptionList.vue"; import { useStore } from "./store"; import { ActionTypes } from "./store/actions"; export default defineComponent({ components: { PrescriptionList }, setup() { const store = useStore(); const loading = computed(() => store.state.loading); onMounted(() => store.dispatch(ActionTypes.GetPrescriptions)); const completedCount = computed(() => store.getters.completedPrescriptionCount); const totalCount = computed(() => store.getters.totalPrescriptionCount); return { loading, completedCount, totalCount }; }, }); </script> <template> <nav class="navbar" role="navigation" aria-label="main navigation"> <div class="navbar-brand"> <a class="navbar-item" href="https://bulma.io"> <img src="https://bulma.io/images/bulma-logo.png" width="112" height="28" /> </a> </div> <div id="navbarBasicExample" class="navbar-menu"> <div class="navbar-start"> <a class="navbar-item"> Home </a> <a class="navbar-item"> About </a> </div> </div> </nav> <div class="container mx-auto mt-4"> <h1 class="is-size-3 has-text-centered p-2 has-text-weight-bold is-success">Vet clinic Frontend</h1> <h3 class="has-text-centered p-2">Manage records of treated livestock in your farm</h3> <div v-if="loading"> <h3 class="has-text-centered mt-4">Loading...</h3> </div> <div v-else> <p class="has-text-centered mt-2">{{ completedCount }} of {{ totalCount }} treated.</p> <PrescriptionList /> </div> </div> </template> <style> @import "~bulma/css/bulma.css"; </style> ที่นี่เราสร้าง Three Computed Properties:
completedCountเรียกcompletedPrescriptionCountGetter Method Retrieve Total Completed TreatmentstotalCountเรียกtotalPrescriptionCountGetter Method Retrieve Total PrescriptionsloadingGets Loading Property of State
With Vue 3 Composition API Methods และ Properties ที่ Template Require ต้อง Return เพื่อ Accessible in Template Notice How We Returned loading, completedCount, and totalCount
PrescriptionList Component
Component นี้ Responsible for Retrieve List of Prescriptions from Backend และ Pass Prescription Data ให้ Child Component
Create PrescriptionList.vue Inside Components Folder With Code:
//src/components/PrescriptionList.vue <template> <table class="table is-hoverable is-striped"> <thead> <tr> <th><abbr title="Position">Prescription Id</abbr></th> <th>Treated</th> <th>Prescription</th> <th><abbr title="Won">Prescribed By</abbr></th> <th><abbr title="Drawn">Disease</abbr></th> <th><abbr title="Drawn">Livestock</abbr></th> <th><abbr title="Lost">Actions</abbr></th> </tr> </thead> <tbody v-if="prescriptions"> <tr v-for="prescription in prescriptions" :key="prescription.id"> <PrescriptionListItem v-bind="prescription" /> </tr> </tbody> <tfoot> <CreateModal v-show="showCreateModal"></CreateModal> <button class="button is-success" @click="setModal">Create Prescription</button> </tfoot> </table> <EditModal v-if="showEditModal" :id="editModalPrescriptionId"></EditModal> <Prescription v-if="showPrescriptionModal" :id="showPrescriptionId"></Prescription> </template> <script> import CreateModal from "./CreateModal"; import EditModal from "./EditModal"; import Prescription from "./Prescription"; import PrescriptionListItem from "./PrescriptionListItem"; import { defineComponent, computed } from "vue"; import { useStore } from "@/store"; import { MutationType } from "@/store/mutations"; export default defineComponent({ name: "Table", components: { CreateModal, PrescriptionListItem, Prescription, EditModal, }, setup() { const store = useStore(); const setModal = () => { store.commit(MutationType.SetCreateModal, true); }; const showCreateModal = computed(() => store.state.showCreateModal); const showEditModal = computed(() => store.state.showEditModal); const editModalPrescriptionId = computed(() => store.state.editModalPrescriptionId); const showPrescriptionModal = computed(() => store.state.showPrescriptionModal); const showPrescriptionId = computed(() => store.state.showPrescriptionId); const prescriptions = computed(() => store.state.prescriptions); return { showCreateModal, setModal, prescriptions, showEditModal, showPrescriptionModal, editModalPrescriptionId, showPrescriptionId }; }, }); </script> <style scoped> table { width: 100%; } .fa { font-size: 1.2rem; margin-left: 15px; } .fa:hover { font-size: 1.4rem; } </style> setModal Method Invokes Mutation ที่ Set showCreateModal in State to True Resulting in Launching Modal สำหรับ Creating Prescriptions
เราสร้าง Computed Properties:
showCreateModalGetsshowCreateModalProperty of StateshowEditModalGetsshowEditModalProperty of StateshowPrescriptionModalGetsshowPrescriptionModalProperty of StateprescriptionGets List of Prescription from StateshowPrescriptionIdGetsshowPrescriptiondProperty of StateeditPrescriptionIdGetseditPrescriptionIdProperty of State
Prescription Component
Component นี้ Receive Prescription ID as Prop from PrescriptionList Component ID Props Used to Fetch Prescription with Corresponding ID via getPrescriptionById Getters Method และ Render Prescription Properties in Browser
Create Prescription.vue Inside Components Folder With Code:
//src/components/Prescription.vue <template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <h1>VIEW PRESCRIPTION</h1> <div class="card"> <div class="card-content"> <div class="media"> <div class="media-content"> <p class="title is-4">Livestock: {{ prescription.livestock }}</p> <p class="subtitle is-6"><b>Prescribed by:</b> {{ prescription.prescribed_by }}</p> <p class="subtitle is-6"><b>Disease:</b> {{ prescription.disease }}</p> </div> </div> <div class="content"> <p class="subtitle is-6">Prescription: {{ prescription.vet_prescription }}</p> </div> </div> </div> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close"></button> </div> </template> <script lang="ts"> import { computed } from "vue"; import { useStore } from "@/store"; import { MutationType } from "@/store/mutations"; export default { name: "PrescriptionModal", props: { id: { type: Number, required: true }, }, setup(props: any) { const store = useStore(); const prescription = computed(() => store.getters.getPrescriptionById(Number(props.id))); const closeModal = () => { store.commit(MutationType.SetPrescriptionModal, { showModal: false, prescriptionId: undefined, }); }; return { closeModal, prescription }; }, }; </script> <style scoped> h1 { color: #ffffff; text-align: center; font-size: 2rem; margin-bottom: 3rem; } </style> closeModal Method Commits SetPrescriptionModal Mutation ซึ่ง Set showModal Property in State to False ในขณะที่ prescription Computed Property เรียก getPrescriptionById Getter Method Retrieve Prescription by Id
CreateModal Component
Component นี้ Responsible for Creating Prescriptions
Create CreateModal.vue Inside Components Folder With Code:
//src/components/CreateModal.vue <template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <form @submit.prevent="createPrescription"> <div class="field"> <label class="label">Prescribed By</label> <div class="control"> <input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" /> </div> </div> <div class="field"> <label class="label">Prescription</label> <div class="control"> <textarea v-model="prescription" class="textarea" placeholder="Enter prescription"></textarea> </div> </div> <div class="field"> <label class="label">Disease</label> <div class="control"> <input v-model="disease" class="input" type="text" placeholder="Enter name of disease" /> </div> </div> <div class="field"> <label class="label">Livestock</label> <div class="control"> <input v-model="livestock" class="input" type="text" placeholder="Enter livestock" /> </div> </div> <div class="field is-grouped"> <div class="control"> <button type="submit" class="button is-link">Submit</button> </div> <div class="control" @click="closeModal"> <button class="button is-link is-light">Cancel</button> </div> </div> </form> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close"></button> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs } from "vue"; import { useStore } from "@/store"; import { Data } from "@/store/state"; import { MutationType } from "@/store/mutations"; import { ActionTypes } from "@/store/actions"; export default { name: "CreateModal", setup() { const state = reactive({ prescription: "", prescribedBy: "", disease: "", livestock: "", }); const store = useStore(); const createPrescription = () => { if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return; const prescription: Data = { vet_prescription: state.prescription, prescribed_by: state.prescribedBy, disease: state.disease, livestock: state.livestock, }; store.dispatch(ActionTypes.CreatePrescription, prescription); state.prescription = ""; state.prescribedBy = ""; state.disease = ""; state.livestock = ""; }; const closeModal = () => { store.commit(MutationType.SetCreateModal, false); }; return { closeModal, ...toRefs(state), createPrescription }; }, }; </script> createPrescription Method Dispatches Action ที่ Make Post Request to Server Thereby Creating New Prescription ในขณะที่ closeModal Method Commits SetPrescriptionModal Mutation (ซึ่ง Set ShowModal Property in State to False)
Working with Forms and Input Elements Requires Two-way Data Binding และ As Such เราใช้ Vue 3's reactive Method Store Values Used in Input Fields
Note: When Using reactive We Need to Use toRefs to Convert Reactive Object to Plain Object ที่ Each Property on Resulting Object Is a Ref Pointing to Corresponding Property in Original Object
EditModal Component
Component นี้ Responsible for Updating Prescriptions Logic คล้าย CreatePrescription Component ที่ Discussed ใน Previous Section
Create EditModal.vue Inside Components Folder With Code:
//src/components/EditModal.vue <template> <div class="modal is-active"> <div class="modal-background"></div> <div class="modal-content"> <form @submit.prevent="updatePrescription"> <h1>Edit Modal</h1> <div class="field"> <label class="label">Prescribed By</label> <div class="control"> <input v-model="prescribedBy" class="input" type="text" placeholder="Enter prescriber's name" /> </div> </div> <div class="field"> <label class="label">Prescription</label> <div class="control"> <textarea v-model="prescription" class="textarea" placeholder="Enter Prescription"></textarea> </div> </div> <div class="field"> <label class="label">Disease</label> <div class="control"> <input v-model="disease" class="input" type="text" placeholder="Enter name of disease" /> </div> </div> <div class="field"> <label class="label">Livestock</label> <div class="control"> <input v-model="livestock" class="input" type="text" placeholder="Enter livestock" /> </div> </div> <div class="field is-grouped"> <div class="control"> <button type="submit" class="button is-link">Submit</button> </div> <div class="control" @click="closeModal"> <button class="button is-link is-light">Cancel</button> </div> </div> </form> </div> <button class="modal-close is-large" @click="closeModal" aria-label="close"></button> </div> </template> <script lang="ts"> import { defineComponent, reactive, toRefs, computed, onMounted } from "vue"; import { useStore } from "@/store"; import { Prescription } from "@/store/state"; import { MutationType } from "@/store/mutations"; import { ActionTypes } from "@/store/actions"; export default { name: "EditModal", props: { id: { type: Number, required: true }, }, setup(props: any) { const state = reactive({ prescription: "", prescribedBy: "", disease: "", livestock: "", }); const store = useStore(); const setFields = () => { const prescription = store.getters.getPrescriptionById(Number(props.id)); if (prescription) { console.log("prescription si kolo", prescription); state.prescription = prescription.vet_prescription; state.prescribedBy = prescription.prescribed_by; state.disease = prescription.disease; state.livestock = prescription.livestock; } }; onMounted(() => { setFields(); }); const updatePrescription = () => { if (state.prescription === "" || state.prescribedBy === "" || state.disease === "" || state.livestock === "") return; const prescription: Prescription = { id: props.id, vet_prescription: state.prescription, prescribed_by: state.prescribedBy, disease: state.disease, livestock: state.livestock, completed: false, editing: false, }; store.dispatch(ActionTypes.UpdatePrescription, prescription); state.prescription = ""; state.prescribedBy = ""; state.disease = ""; state.livestock = ""; }; const closeModal = () => { store.commit(MutationType.SetEditModal, { showModal: false, prescriptionId: undefined }); }; return { closeModal, ...toRefs(state), updatePrescription }; }, }; </script> <style scoped> label { color: #ffffff; } h1 { color: #ffffff; text-align: center; font-size: 2rem; margin-bottom: 3rem; } </style> createPrescription Method Dispatches Action ที่ Make Put Request to Server Thereby Updating Existing Prescription by its ID ในขณะที่ closeModal Method Commits SetPrescriptionModal Mutation That Sets showModal Property in State to False Calling setFields Method on onMounted Lifecycle Hook Triggers getPrescriptionById Getters Method Fetch Prescription from Store by its ID และ Then Updates Properties in Reactive Object with Fetched Prescription Properties As Soon As Component Is Mounted on DOM
Launching the Final Project
ใน Root Directory ของ Rails API Server Run Command Start Server:
rails server ตอนนี้ Run Frontend Application With Command:
npm run serve Finally Application ควรเป็น:

Conclusion
เราสร้าง CRUD API Server ด้วย Rails และ Frontend Application บน Vue 3 Composition API และ Vuex ทั้งหมดรัน Different Servers ในขณะที่ Integrate Both Stacks Build Fullstack Web Application ผมหวังว่าคุณเรียนรู้มากจาก Tutorial นี้ Please Reach Out in Comment Section Below ถ้ามี Questions หรือ Suggestions Here is the GitHub Repo for the Full-stack Application Built in This Tutorial