- 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
import
components where you'd like to use them - Vue introduces some magic so that
@
is an alias to thesrc
folder (Seevite.config
if 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.vue
component insrc/components/HelloWorld.vue
with thistemplate
<template>Hello VueJs!</template>
- Use that in
App.vue
by replacing the line that says<!-- When using HelloWorld use that component here -->
- Refactor
HelloWorld.vue
to 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.vue
by 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,
ref
andreactive
- 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 ofsetup
in there. - Make the data of
PeopleList
a reactive arrayfriends
where each object looks like one fromserver/api/db.json
- To make it reactive use the
ref
API offered from Vue - You'll have to import
ref
first 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-for
construct - There is another construct called
v-bind
that allows you to "bind" and attribute to an HTML element that is dynamically allocated- You can potentially supply
v-for
av-bind:key
so that Vue can optimize re-rendering—this isn't strictly necessary, but generally a good idea. - There is a shortcut for
v-bind:key
namely:key
- You can potentially supply
v-for
also provides a a mechanism to get theindex
of the element — you can use this to do interesting things- There is also a
v-if
allows us to conditionally render elements using a boolean
- Use the
v-for
to create onli
element by looping over all thefriends
in the state and display thefirstName
andindex
of each friend - Use
v-if
to only display ahr
element 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:event
syntax, and attach a handler to it- Like
v-bind:attr
has a shortcut (:attr
),v-on:event
has a shortcut, namely@event
- Like
- Attach a
click
handler so that when you click on thev-btn
you invoke a handler calledlike
that toggles thefav
property of thefriend
you clicked on - Can you figure out how to bind the color on
v-icon
so that it switches betweenred
orgrey
depending onfriend.fav
property
-
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
defineProps
method that is automatically available to you.defineProps
is supplied an array of all props the component needs. props
are just bindings, so you canv-bind
theprops
from the parent component to pass those values in
- Create a
PersonItem.vue
file next toPeopleList.vue
and extract all the code that displays a friend into it- You want to grab all the code in
v-list-item
element (Be sure to strip out thev-for
) - Declare the props for
PersonItem
usingdefineProps
so thatPeopleList
can pass in afriend
and a conditional calledlast
that 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-divider
to uselast
prop - Be sure to have a
like
method inPersonItem.vue
since your@click
handler expects it
- You want to grab all the code in
- Be sure to "import"
PersonItem
inPeopleList
and use that with av-for
like so<PersonItem v-for="(friend, index) of friends" />
- Do NOT forget to
v-bind
theprops
that your child component needs
- Do NOT forget to
- You can declare your
props
with some validation. - In this case,
defineProps
can 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 isrequired
or not, and adefault
value.
- 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";
computed
takes a function which should return the value of the property and returns the computed property
- Use a "computed" property in
PersonItem.vue
to 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
PersonItem
a good citizen—it's currently modifying thefriend
prop in it'slike
method. Instead of modifying thefriend
prop, emit an event calledfriend-liked
- Make sure
PeopleList
is 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
friends
inPeopleList
with a call usingaxios
(this is already in yourpackage.json
).- The end-point is
http://localhost:3000/friends
. - Be sure to introduce the
onMounted
lifecycle hook.- You'll need to
import { onMounted } from "vue";
- You'll need to
axios.get
returns you aresp
object which has the payload in it'sdata
property- To replace the value of a
ref
you need to set it'svalue
property
- The end-point is
- See if you can figure out how to update to a friend using the
patch
method inaxios
in thePeopleList#like
— The endpoint you are looking for ishttp://localhost:3000/friends
but you have to pass thefriend.id
as part of the URL (likehttp://localhost:3000/friends/1
) and you have to send the updated property. For example, to update thefav
property, 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
Dashboard
component in thecomponents
folder 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.js
with 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 theDashboard
component, and one to/people
that uses thePeopleList
component -
REPLACE
main.js
with 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
items
array inApp.vue
with
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!" },
];