- Repository resides here - https://github.com/looselytyped/web-apps-with-vue3
- Link to this Gist - https://gist.github.com/looselytyped/3374c6803523164c373d768f316a1a49
- Getting started with
create-vue - What do you have in the project you cloned?
- Creating new components, and constructing Component hierarchies
- You have to
importcomponents where you'd like to use them - Vue introduces some magic so that
@is an alias to thesrcfolder (Seevite.configif curious) - You can use custom tags using PascalCase (
HelloWorld) or kebab-case (<hello-world />)—Vue will resolve it correctly regardless
- You have to
- Before we get started—heads up—we'll be using the Composition API (as opposed to the traditional Options API) for today's workshop
- Introduce a
HelloWorld.vuecomponent insrc/components/HelloWorld.vuewith thistemplate
<template>Hello VueJs!</template>- Use that in
App.vueby replacing the line that says<!-- When using HelloWorld use that component here -->
- Refactor
HelloWorld.vueto bePeopleList.vue(be sure to replace the custom element inApp.vue) - Use this template
<template>
<v-container fluid>
<v-row no-gutters class="flex-nowrap">
<v-card class="d-flex justify-start mx-4" style="width: 100%">
<v-list header style="width: 100%">
<!-- Display the first friend's first and last name here -->
</v-list>
</v-card>
<div class="d-flex justify-end mb-6">
<v-btn color="success" dark large>Add Friend</v-btn>
</div>
</v-row>
</v-container>
</template>- Use that in
App.vueby replacing the line that says<!-- When using PeopleList replace this entire block -->
- To bind to the template, you need data
- This data can be "static" (in other words, once rendered, an update to the data will not be reflected) in the template
- OR it can be "reactive" (that is, if the data changes, the template reflects that change)
- Vue offers two APIs to make data reactive,
refandreactive
- Vue offers two APIs to make data reactive,
<script setup>
import { ref } from "vue";
const count = ref(0);
console.log(count.value);
</script><template>
<!-- Template uses the reactive count -->
<h1>{{ count }}</h1>
<!-- Clicking the button increments the counter and the template automatically updates -->
<button @click="count++" type="button">Add 1 to counter</button>
</template>- Introduce the
<script setup></script>block inPeopleList.vue. Note the use ofsetupin there. - Make the data of
PeopleLista reactive arrayfriendswhere each object looks like one fromserver/api/db.json- To make it reactive use the
refAPI offered from Vue - You'll have to import
reffirst usingimport { ref } from "vue";
- To make it reactive use the
- Display the first friend's first and last name in the template
- To loop in Vue there is a
v-forconstruct - There is another construct called
v-bindthat allows you to "bind" and attribute to an HTML element that is dynamically allocated- You can potentially supply
v-forav-bind:keyso that Vue can optimize re-rendering—this isn't strictly necessary, but generally a good idea. - There is a shortcut for
v-bind:keynamely:key
- You can potentially supply
v-foralso provides a a mechanism to get theindexof the element — you can use this to do interesting things- There is also a
v-ifallows us to conditionally render elements using a boolean
- Use the
v-forto create onlielement by looping over all thefriendsin the state and display thefirstNameandindexof each friend - Use
v-ifto only display ahrelement every BUT the last element
- Now let's pretty it up. Replace your hard work with the following
<!-- REPLACE ONLY THE UL and LI elements you wrote WITH THIS. -->
<v-list-item v-for="(friend, index) of friends" :key="friend.id">
<v-list-item-title class="d-flex justify-start">
{{ friend.firstName }}
{{ friend.lastName }}
<v-spacer />
<div class="d-flex justify-end">
<v-btn density="comfortable" variant="plain" icon>
<v-icon class="edit"> mdi-pencil </v-icon>
</v-btn>
<v-btn
density="comfortable"
variant="plain"
icon
>
<v-icon class="fav" color="red">
mdi-heart
</v-icon>
</v-btn>
</div>
</v-list-item-title>
<!-- eslint-disable-next-line vue/valid-v-for -->
<v-divider v-if="index !== friends.length - 1"></v-divider>
</v-list-item>-
We spoke about state management via reactive state.
-
"Method handlers" allow you operate on that "state"
- The cool thing is that they are just regular JavaScript functions. Woot!
-
To "get" an event, you use parentheses
v-on:eventsyntax, and attach a handler to it- Like
v-bind:attrhas a shortcut (:attr),v-on:eventhas a shortcut, namely@event
- Like
- Attach a
clickhandler so that when you click on thev-btnyou invoke a handler calledlikethat toggles thefavproperty of thefriendyou clicked on - Can you figure out how to bind the color on
v-iconso that it switches betweenredorgreydepending onfriend.favproperty
-
With "state" and "methods" our Vue instance is a ViewModel — it has "instance" state, and "methods" operate on that data.
-
Next, can we simplify this component?
- Do we need another component?
- Whats the reuse potential?
- Is there enough state to manage?
- How do we loop over a list of items?
- If we are looping over a child component, how do we supply it with what it needs?
- Do we need another component?
Let's talk about refactoring components
- Child elements may need "inputs" from parents.
These are called
props - In the composition API, you have a specific
definePropsmethod that is automatically available to you.definePropsis supplied an array of all props the component needs. propsare just bindings, so you canv-bindthepropsfrom the parent component to pass those values in
- Create a
PersonItem.vuefile next toPeopleList.vueand extract all the code that displays a friend into it- You want to grab all the code in
v-list-itemelement (Be sure to strip out thev-for) - Declare the props for
PersonItemusingdefinePropsso thatPeopleListcan pass in afriendand a conditional calledlastthat tells you if this is the last friend—you can use this to decide whether or not to display thev-divider - Change the conditional in the
v-dividerto uselastprop - Be sure to have a
likemethod inPersonItem.vuesince your@clickhandler expects it
- You want to grab all the code in
- Be sure to "import"
PersonIteminPeopleListand use that with av-forlike so<PersonItem v-for="(friend, index) of friends" />- Do NOT forget to
v-bindthepropsthat your child component needs
- Do NOT forget to
- You can declare your
propswith some validation. - In this case,
definePropscan take an object, in which every key is the name of the prop, and it's value is a nested object that defines it'stype, whether isrequiredor not, and adefaultvalue.
- Convert your props to use prop validation
- Vue offers you a way to "compute" (a.k.a derived) properties to avoid cluttering the template with complex logic
- They are called computed properties for a reason!
- Computed properties are different than methods b/c they only react to the properties they are "watching"
- Just like
ref(andreactive) you canimport { computed } from "vue";computedtakes a function which should return the value of the property and returns the computed property
- Use a "computed" property in
PersonItem.vueto calculate thefullName- Be sure
import { computed } from "vue";
- Be sure
You should not modify the props supplied to a component! It's not the components state—it's the parents. Vue warns you of this, and the resultant behavior can be unpredictable. Instead, you should "emit" events that notify the parent that it (the parent) should modify it's state.
If props are "inputs" to components, then you can "emit" events, which act as "outputs"
Much like you defineProps, you can also defineEmits—both of these serve as the API of the component.
props are the inputs the component needs, emits are the output.
The component can simply emit the event (See below), and it's parent can treat this like any other event using v-on like so
// child component, for example called ExampleComponent
const emit = defineEmits(["some-event"]);
emit('some-event', someValue);
// parent component
<example-component @some-event="someHandler()">
- Make
PersonItema good citizen—it's currently modifying thefriendprop in it'slikemethod. Instead of modifying thefriendprop, emit an event calledfriend-liked- Make sure
PeopleListis listening for that event, and reacts appropriately
- Make sure
Performing asynchronous operations, like communicating with the backend is usually done using standard browser APIs (like fetch) or third-party libraries (like axios).
Vue does not have any in-built support for this.
In our case, there is an endpoint at http://localhost:3000/friends—you can fetch all your friends from there.
If you have not, this is the time to run npm run rest-endpoint in a second terminal.
We need a mechanism that allows us to intercept the lifecycle of the component.
Vue gives us a few methods, one of which is onMounted.
We can use this as a place to put our Ajax calls.
Before you do this, be sure you have npm run rest-endpoint in a terminal.
- Replace the hard-coded array of
friendsinPeopleListwith a call usingaxios(this is already in yourpackage.json).- The end-point is
http://localhost:3000/friends. - Be sure to introduce the
onMountedlifecycle hook.- You'll need to
import { onMounted } from "vue";
- You'll need to
axios.getreturns you arespobject which has the payload in it'sdataproperty- To replace the value of a
refyou need to set it'svalueproperty
- The end-point is
- See if you can figure out how to update to a friend using the
patchmethod inaxiosin thePeopleList#like— The endpoint you are looking for ishttp://localhost:3000/friendsbut you have to pass thefriend.idas part of the URL (likehttp://localhost:3000/friends/1) and you have to send the updated property. For example, to update thefavproperty, you'dpatch{ fav: friend.fav }.
- Routing gives you a mechanism to replaces pieces of the DOM based on the application URL
- Routing is provided by a plugin, namely
vue-router— This has some configuration you need to apply with it- The primary piece of configuration that affects us is to define which "path" uses which component
Introduce the following files
- Create a new
Dashboardcomponent in thecomponentsfolder with the following content:
<script setup>
import { ref } from "vue";
import { computed } from "vue";
import { onMounted } from "vue";
import axios from "axios";
const friends = ref([]);
const favCount = computed(() => friends.value.filter((f) => f.fav).length);
onMounted(async () => {
const resp = await axios.get("http://localhost:3000/friends");
friends.value = resp.data;
});
</script>
<template>
<v-container fluid>
<v-row>
<v-col cols="2">
<v-card>
<v-card-item title="Contacts"></v-card-item>
<v-card-text class="py-3">
<v-row>
<v-col class="text-h2">{{ friends.length }}</v-col>
<v-col class="text-right">
<v-icon color="error" icon="mdi-contacts" size="60"></v-icon>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<v-col cols="2">
<v-card>
<v-card-item title="Favs"></v-card-item>
<v-card-text class="py-3">
<v-row>
<v-col class="text-h2">{{ favCount }}</v-col>
<v-col class="text-right">
<v-icon color="error" icon="mdi-heart" size="60"></v-icon>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
<style></style>
- Introduce a new file called
src/router/index.jswith the following content:
import { createRouter, createWebHistory } from "vue-router";
import Dashboard from "@/components/Dashboard.vue";
import PeopleList from "@/components/PeopleList.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
// introduce routes here for the following — DO NOT FORGET TO NAME THEM!
// "/" uses "Dashboard"
// "/people" uses "PeopleList"
// all other routes redirects to "/"
// Here is an example
// {
// path: "/",
// name: "dashboard",
// component: Dashboard
// },
],
});
export default router;-
Introduce two paths—one to
/that uses theDashboardcomponent, and one to/peoplethat uses thePeopleListcomponent -
REPLACE
main.jswith the following
import { createApp } from "vue";
import App from "./App.vue";
// Vuetify
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import "@mdi/font/css/materialdesignicons.css"; // Ensure you are using css-loader
import router from "./router"; // import router
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: "mdi",
},
});
const app = createApp(App).use(vuetify).use(router); // have the app use the router
app.mount("#app");-
Be sure to use
<router-view />inApp.vue -
Replace
<v-list-item v-else :value="item">inApp.vue(Line 20) with<v-list-item v-else :value="item" :to="{ name: item.routeName }"> -
Replace the
itemsarray inApp.vuewith
const items = [
{ icon: "mdi-view-dashboard", text: "Dashboard", routeName: "dashboard" },
{ icon: "mdi-contacts", text: "Contacts", routeName: "people" },
{ divider: true },
{ icon: "mdi-clock", text: "Stay tuned!" },
];