An updated one using Vue3 can be found here
- Link to this Gist itself - https://tinyurl.com/vue2-workshop
- Repository resides here - https://github.com/looselytyped/web-apps-with-vue
- Vue and Vue-CLI
- Getting started with the Vue CLI
- What do you have in the project you cloned?
- Creating new components, and constructing Component hierarchies
- Introduce a
HelloWorld.vue
component insrc/components/HelloWorld.vue
with thistemplate
<template>
<v-container>
Hello VueJs!
</v-container>
</template>
- Use that in
App.vue
by replacing the line that says<!-- Insert your code here -->
- Refactor
HelloWorld.vue
to bePeopleList.vue
(be sure to replace the custom element inApp.vue
) - Use this template
<template>
<v-container fluid>
<v-layout row>
<v-flex grow pa-1>
<v-card>
<v-list header>
<!-- Display the first friend's first and last name here -->
</v-list>
</v-card>
</v-flex>
<v-flex shrink pa-1>
<v-btn color="success" dark large>Add Friend</v-btn>
</v-flex>
</v-layout>
</v-container>
</template>
- Update
App.vue
so that you don't have nestedv-container
(there is one inApp.vue
and one inPeople.vue
)
data
is the "state" of the component- To declare the "state"
data
needs to be a function
export default {
// this is how you declare state
data() {
return {
// typically you return a collection, usually an object
};
}
}
- Make the state of
PeopleList
an arrayfriends
where each object looks like one fromserver/api/db.json
- 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 allocatedv-for
needsv-bind:key
so that Vue can optimize re-rendering- There is a shortcut for
v-bind:key
namely:key
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. -->
<template v-for="(friend, index) in friends">
<v-list-item :key="friend.id">
<v-list-item-content>
<v-list-item-title>
{{ friend.firstName }} {{ friend.lastName }}
</v-list-item-title>
</v-list-item-content>
<v-btn icon>
<v-icon class="edit">
mdi-pencil
</v-icon>
</v-btn>
<v-btn icon>
<v-icon class="fav" color="grey">
mdi-heart
</v-icon>
</v-btn>
</v-list-item>
<!-- eslint-disable-next-line vue/valid-v-for -->
<v-divider v-if="index !== friends.length - 1"></v-divider>
</template>
-
We spoke about state management via
data
. -
methods
allow you operate on that "state" — This is a new block that we are going to use next -
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 "data" 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
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- Declare
props
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
- Be sure to have a
like
method inPersonItem.vue
since your@click
handler expects it
- Declare
- Be sure to "import"
PersonItem
inPeopleList
and use that with av-for
like so<PersonItem v-for="(friend, index) in friends" />
- Do NOT forget to
v-bind
theprops
that your child component needs
- Do NOT forget to
- You can declare your
props
as typed objects
- Convert your props to typed objects
- Vue offers you a way to "compute" (a.k.a derived) properties
- Computed properties are different than methods b/c they only react to the properties they are "watching"
- Use a "computed" property in
PersonItem.vue
to calculate the "fullName"
Smart vs. dumb components
- Smart components
- Usually leverage services or know about endpoints
- Know how to get/load/update data
- Dumb components
- Fully defined via their API
- Everything is a
prop
or anevent
If props
are inputs to components, then you can "emit" events, which act as "outputs"
The API for the same is $emit
on the Vue instance itself.
Parents can treat this like any other event using v-on
like so
// child component, for example called ExampleComponent
this.$emit('some-event', someValue);
// parent component
<example-component @some-event="someHandler()">
- Make
PersonItem
dumb. Instead of modifying thefriend
prop, emit it, and update it in the parent component
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.
We need a mechanism that allows us to intercept the lifecycle of the component.
Vue gives us a few methods, one of which is mounted
.
We can use this as a place to put our Ajax calls.
Before you do this, be sure you have yarn run server
or npm run server
in a terminal.
- Replace the hard-coded array of
friends
inPeopleList
with a call usingaxios
(this is already in yourpackage.json
). The end-point ishttp://localhost:3000/friends
. Be sure to introduce themounted
lifecycle hook.
- 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. Here is a snippet you can use
await axios.patch(`http://localhost:3000/friends/${friend.id}`, {
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:
<template>
<v-container grid-list-xl fluid>
<v-layout row wrap>
<v-flex lg3 sm6 xs12>
<v-card>
<v-card-text class="pa-0">
<v-container class="pa-0">
<div class="layout row ma-0">
<div class="sm6 xs6 flex">
<div class="layout column ma-0 justify-center align-center">
<v-icon color="indigo" size="56px">contacts</v-icon>
</div>
</div>
<div class="sm6 xs6 flex text-sm-center py-3">
<div class="headline">Friends</div>
<span class="caption">{{ friends.length }}</span>
</div>
</div>
</v-container>
</v-card-text>
</v-card>
</v-flex>
<v-flex lg3 sm6 xs12>
<v-card>
<v-card-text class="pa-0">
<v-container class="pa-0">
<div class="layout row ma-0">
<div class="sm6 xs6 flex">
<div class="layout column ma-0 justify-center align-center">
<v-icon color="pink" size="56px">favorite</v-icon>
</div>
</div>
<div class="sm6 xs6 flex text-sm-center py-3">
<div class="headline">Favs</div>
<span class="caption">{{ favCount }}</span>
</div>
</div>
</v-container>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
friends: []
};
},
computed: {
favCount() {
return this.friends.filter(f => f.fav).length;
}
},
async mounted() {
const resp = await axios.get("http://localhost:3000/friends");
this.friends = resp.data;
}
};
</script>
<style></style>
- Introduce a new file called
src/router.js
with the following content:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.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
// },
]
});
- REPLACE
main.js
with the following
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import router from "./router";
Vue.config.productionTip = false;
new Vue({
vuetify,
router,
render: h => h(App)
}).$mount("#app");
- Be sure to use
<router-view />
inApp.vue
- Replace
<v-list-item :key="i" v-else :id="item.text.toLowerCase()">
inApp.vue
(Line 20) with
<v-list-item
:key="i"
v-else
:to="{ name: item.routeName }"
:id="item.text.toLowerCase()"
>
- Replace the
items
array inApp.vue
with
items: [
{ icon: "mdi-view-dashboard", text: "Dashboard", routeName: "Dashboard" },
{ icon: "mdi-contacts", text: "Contacts", routeName: "People" },
{ divider: true },
{ icon: "mdi-message-bulleted", text: "Journal" }
]
- How do you go about adding/editing friends? Ideally we want
/people/add
and/people/:id/edit
- Which means we need nested routes under
/people
- But where do we put the
router-view
element?
- Which means we need nested routes under
- Create a new file
views/People.vue
that looks like this
<template>
<v-container fluid>
<router-view />
</v-container>
</template>
- Modify
router.js
to have a child route, like so:
{
path: "/people",
component: People,
children: [
{
path: "",
name: "People",
component: PeopleList
}
]
},
- Remove the
v-container
tags fromcomponents/PeopleList.vue
template
- To add we need a new child route
/people/add
and a corresponding component
- Create a new component
components/AddEditFriend.vue
with some boilerplate tempalte - Add the child route
/people/add
that usesAddEditFriend
as the component with the nameAddEditFriend
- Modify
PeopleList.vue
so clicking thev-btn
routes to the theAddEditFriend
route, like so:
<v-btn color="success" large :to="{ name: 'AddEditFriend' }">
Add Friend
</v-btn>
- Vue has no "forms" support. You get two-way binding with
v-model
though - You might reach into a 3rd party library to make managing forms easier
<template>
<v-form ref="form" v-model="valid">
<v-text-field
v-model="selectedFriend.firstName"
:rules="nameRules"
label="First name"
id="firstName"
required
></v-text-field>
<v-text-field
v-model="selectedFriend.lastName"
:rules="nameRules"
label="Last name"
id="lastName"
required
></v-text-field>
<v-checkbox
v-model="selectedFriend.fav"
label="Fav?"
id="fav"
required
></v-checkbox>
<v-radio-group v-model="selectedFriend.gender" id="gender" row>
<v-radio label="Male" :value="genders.male"></v-radio>
<v-radio label="Female" :value="genders.female"></v-radio>
<v-radio label="Undisclosed" :value="genders.undisclosed"></v-radio>
</v-radio-group>
<v-btn class="ma-2" color="error" @click="reset">
Reset Form
</v-btn>
<v-btn
class="ma-2"
color="success"
:disabled="!valid"pvue
id="submit"
@click="submit"
>
Submit
</v-btn>
<v-btn class="ma-2" :to="{ name: 'People' }">Cancel</v-btn>
</v-form>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
valid: true,
selectedFriend: {
firstName: "",
lastName: "",
gender: "male",
fav: false
},
genders: {
male: "male",
female: "female",
undisclosed: "undisclosed"
},
nameRules: [v => !!v || "Name is required"]
};
},
methods: {
reset() {
this.$refs.form.reset();
},
async submit() {
const submission = {
...this.selectedFriend
};
await axios.post("http://localhost:3000/friends", submission);
this.$router.push({ name: "People" });
}
}
};
</script>
<style></style>