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.vuecomponent insrc/components/HelloWorld.vuewith thistemplate
<template>
<v-container>
Hello VueJs!
</v-container>
</template>- Use that in
App.vueby replacing the line that says<!-- Insert your code here -->
- Refactor
HelloWorld.vueto 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.vueso that you don't have nestedv-container(there is one inApp.vueand one inPeople.vue)
datais the "state" of the component- To declare the "state"
dataneeds 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
PeopleListan arrayfriendswhere 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-forconstruct - There is another construct called
v-bindthat allows you to "bind" and attribute to an HTML element that is dynamically allocatedv-forneedsv-bind:keyso that Vue can optimize re-rendering- There is a shortcut for
v-bind:keynamely:key
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. -->
<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. -
methodsallow 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: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 "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 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- Declare
propsso 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 - Be sure to have a
likemethod inPersonItem.vuesince your@clickhandler expects it
- Declare
- Be sure to "import"
PersonIteminPeopleListand use that with av-forlike so<PersonItem v-for="(friend, index) in friends" />- Do NOT forget to
v-bindthepropsthat your child component needs
- Do NOT forget to
- You can declare your
propsas 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.vueto 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
propor 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
PersonItemdumb. Instead of modifying thefriendprop, 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
friendsinPeopleListwith a call usingaxios(this is already in yourpackage.json). The end-point ishttp://localhost:3000/friends. Be sure to introduce themountedlifecycle hook.
- 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. 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
Dashboardcomponent in thecomponentsfolder 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.jswith 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.jswith 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
itemsarray inApp.vuewith
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/addand/people/:id/edit- Which means we need nested routes under
/people - But where do we put the
router-viewelement?
- Which means we need nested routes under
- Create a new file
views/People.vuethat looks like this
<template>
<v-container fluid>
<router-view />
</v-container>
</template>
- Modify
router.jsto have a child route, like so:
{
path: "/people",
component: People,
children: [
{
path: "",
name: "People",
component: PeopleList
}
]
},
- Remove the
v-containertags fromcomponents/PeopleList.vuetemplate
- To add we need a new child route
/people/addand a corresponding component
- Create a new component
components/AddEditFriend.vuewith some boilerplate tempalte - Add the child route
/people/addthat usesAddEditFriendas the component with the nameAddEditFriend - Modify
PeopleList.vueso clicking thev-btnroutes to the theAddEditFriendroute, 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-modelthough - 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>