NOTES
-
This requires a project created by the Vue CLI
-
I have this project — It is webpack based
-
Vue CLI works in tandem with
package.json
and NPM/Yarn, as well as thevue-cli-service
to make its magic happen -
The thing to bear in mind here is that we use SFCs
-
This is much like Angular where you write HTML/CSS/JS in separate blocks.
-
However it does complicate the tooling since you need your editor to be aware of the different blocks.
-
git checkout master
-
Clear out
HelloWorld.vue
,App.vue
-
In
App.vue
<template>
<div id="app">
<h1>Hello!</h1>
</div>
</template>
- SHOW
main.js
— This is where the "mounting" happens. - And the
index.html
file - Show Elements tab in browser and show how
app.js
has been injected - Show Vetur extension in VS Code
- In
HelloWorld.vue
<template>
<v-container>
Hello VueJs!
</v-container>
</template>
<script>
export default {};
</script>
<style></style>
- In
App.vue
<template>
<HelloWorld />
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
components: {
HelloWorld
}
};
</script>
stash save --include-untracked
git checkout 4211172ea101d4bfbebfe541053f3bd07ed60bbd
Then CREATE ANOTHER CHILD COMPONENT AND USE IT IN APP
Couple of things to note
- We do NOT see
HelloWorld
in the DOM. This is different than Angular
<template>
<!-- Blatantly stolen from https://vuetifyjs.com/en/examples/layouts/sandbox -->
<v-app id="sandbox">
<v-navigation-drawer
v-model="primaryDrawer.model"
:permanent="primaryDrawer.type === 'permanent'"
:temporary="primaryDrawer.type === 'temporary'"
absolute
overflow
app
>
<div class="pa-3 text-xs-center teal white--text">
<div class="display-2 py-4">
Friends HQ
</div>
<p>Together, we are stronger</p>
</div>
<v-spacer></v-spacer>
<v-list dense> </v-list>
</v-navigation-drawer>
<v-toolbar app absolute>
<v-toolbar-side-icon
v-if="primaryDrawer.type !== 'permanent'"
@click.stop="primaryDrawer.model = !primaryDrawer.model"
></v-toolbar-side-icon>
</v-toolbar>
<v-content>
<v-container fluid>
<v-layout align-center justify-center>
<v-flex xs10>
<v-card>
<v-card-text>
<v-layout row wrap>
<HelloWorld />
</v-layout>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
<v-footer :inset="true" app>
<span class="px-3">
© Looselytyped {{ new Date().getFullYear() }}
</span>
</v-footer>
</v-app>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
components: {
HelloWorld
},
data: () => ({
primaryDrawer: {
model: null,
type: "default (no property)"
}
})
};
</script>
- Delete
HelloWorld.vue
- Create
PeopleList.vue
- Update
App.vue
to usePeopleList.vue
PeopleList.vue
- For the
template
<template>
<v-container fluid>
<v-layout row>
<v-flex grow pa-1>
<v-card>
<v-list header>
</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>
- Start with the
friends
array anddata
- inPeopleList.vue
<script>
const friends = [
{
id: 1,
firstName: "Michelle",
lastName: "Mulroy",
gender: "female",
fav: true
},
{
id: 2,
firstName: "Venkat",
lastName: "Subramanian",
gender: "male",
fav: true
},
{
id: 3,
firstName: "Matt",
lastName: "Forsythe",
gender: "none",
fav: false
},
{
id: 4,
firstName: "Nate",
lastName: "Schutta",
gender: "male",
fav: false
}
];
export default {
data: () => {
return {
friends
};
}
};
</script>
In the template we can now do something like this
<v-list header>
<ul>
<li v-for="(friend, index) in friends" v-bind:key="friend.id">
{{ friend.firstName }} {{ index }}
<hr v-if="index === friends.length - 1" />
</li>
</ul>
</v-list>
- We can replace
v-bind:key="friend.id"
with:key="friend.id"
- We are also using
v-if
— there is alsov-else
and `v-else-if - There is also
v-show
ONLY toggles hidden property
<template>
<v-container fluid>
<v-layout row>
<v-flex grow pa-1>
<v-card>
<v-list header>
<template v-for="(friend, index) in friends">
<v-list-tile :key="friend.id" avatar ripple>
<v-list-tile-content>
<v-list-tile-title>
{{ friend.firstName }} {{ friend.lastName }}
</v-list-tile-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon :color="'red'">favorite</v-icon>
</v-list-tile-action>
</v-list-tile>
<!-- eslint-disable-next-line vue/valid-v-for -->
<v-divider v-if="index + 1 < friends.length"></v-divider>
</template>
</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>
export default {
data() {
return {
friends
};
}
};
Now we can listen for a click
using v-on:click="like(friend)"
— Make sure we have a like
method on our Vue instance
We can also use @click
in place of v-on
<template>
<v-container fluid>
<v-layout row>
<v-flex grow pa-1>
<v-card>
<v-list header>
<template v-for="(friend, index) in friends">
<v-list-tile
:key="friend.id"
avatar
ripple
v-on:click="like(friend)" <-- ADD THIS LINE
>
<v-list-tile-content>
<v-list-tile-title
>{{ friend.firstName }}
{{ friend.lastName }}</v-list-tile-title
>
</v-list-tile-content>
<v-list-tile-action>
<v-icon :color="'red'">favorite</v-icon>
</v-list-tile-action>
</v-list-tile>
<!-- eslint-disable-next-line vue/valid-v-for -->
<v-divider v-if="index + 1 < friends.length"></v-divider>
</template>
</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>
export default {
data() {
return {
friends
};
},
methods: { <-- ADD methods block
like(friend) {
console.log(`${friend} changed`);
friend.fav = !friend.fav;
}
}
};
- With "data" and "methods" our Vue instance is a ViewModel — it has "instance" state, and "methods" operate on that data.
NOTICE THAT WE ARE BINDING HERE
In PeopleList.vue
change the v-icon
to look like
<v-icon :color="friend.fav ? 'red' : 'grey'">favorite</v-icon>
Notice that we are binding here
- Create
PersonItem.vue
file
<template>
<div>
<v-list-tile :key="friend.id" avatar ripple @click="like(friend)">
<v-list-tile-content>
<v-list-tile-title
>{{ friend.firstName }} {{ friend.lastName }}</v-list-tile-title
>
</v-list-tile-content>
<v-list-tile-action>
<v-icon :color="friend.fav ? 'red' : 'grey'">favorite</v-icon>
</v-list-tile-action>
</v-list-tile>
<!-- eslint-disable-next-line vue/valid-v-for -->
<v-divider v-if="index + 1 < friends.length"></v-divider>
</div>
</template>
<script>
export default {};
</script>
<style>
</style>
- How do we get the
friend
and know its the last in here? - WE USE PROPS!
In PersonItem.vue
file
export default {
props: ["friend", "last"],
methods: {
like(friend) {
friend.fav = !friend.fav;
}
}
};
- Now change the template to use
last
- Now update
PeopleList.vue
to use that component
import PersonItem from "./PersonItem";
components: {
PersonItem
},
NOTE THAT PROPS are just bindings! We can use v-bind
or simply :
In People.vue
<PersonItem
v-for="(friend, index) in friends"
:key="friend.id"
:friend="friend"
:last="index + 1 < friends.length"
></PersonItem>
You can impose restrictions on props
like so
In PersonItem.vue
file change props
to
props: {
friend: {
type: Object,
required: true,
},
last: {
type: Boolean,
default: false
}
},
- Computed properties ARE PROPERTIES !!! They are NOT METHODS
- They are meant to GIVE A DIFFERENT VIEW of the
data
In PersonItem.vue
file
computed: {
fullName() {
return `${this.friend.firstName}, ${this.friend.lastName}`;
}
}
THere are also filters but there isn't much you can do with filters that you can't do with computed properties
-
How do children talk to parents? Via custom events!
-
In
PersonItem.vue
methods: {
notifyParent() {
this.$emit("notify-parent", this.friend);
}
}
@click.stop="notifyParent"
NOTE THE MODIFIER THERE Show VUE -> Events in Browser
- In
PeopleList.vue
<PersonItem
v-for="(friend, index) in friends"
:key="friend.id"
:friend="friend"
:last="index + 1 < friends.length"
@notify-parent="like" <----- ADD THIS LINE
></PersonItem>
Idiomatically, event names are hyphen-case
stash save --include-untracked
git checkout 9138a6fceb27787cef7cb74c5ed533941fb207aa
Then Add another prop
to PersonItem
with a default value and supply it from the parent
-
We will use one of the life-cycle methods, namely mounted
-
In
PersonList.vue
add
import axios from "axios";
mounted() {
axios.get("http://localhost:3000/friends").then(response => {
this.friends = response.data;
});
}
You can use async/await
here
async mounted() {
this.friends = (await axios.get("http://localhost:3000/friends")).data;
},
- In
PersonList.vue
updatelike
method to do aPUT
axios.put(`http://localhost:3000/friends/${friend.id}`, friend);
The nice thing here is that we can even use window.http
— no Angular modules and any such non-sense
- Make
Dashboard.vue
<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";
const friends = [];
export default {
data: () => {
return {
friends
};
},
computed: {
favCount() {
return this.friends.filter(f => f.fav).length;
}
},
mounted() {
axios.get("http://localhost:3000/friends").then(response => {
this.friends = response.data;
});
}
};
</script>
<style></style>
Note here we have a computed
property
-
We can install
vue-router
usingvue add router
-
Introduce a
router.js
file next tomain.js
-
NOTE here we are configuring the global Vue Object!
-
Vue Router is a plugin
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
export default new Router({
mode: "history",
base: process.env.BASE_URL,
routes: [
{
path: "/",
name: "Dashboard",
component: Dashboard
},
{
path: "/people",
name: "People",
component: PeopleList
},
{
path: "*",
redirect: "/"
}
]
});
We are using Named routes here
- Next we tell our app about our routes - edit
main.js
and supplyrouter
to our global Vue instance
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App)
}).$mount("#app");
- Finally we use it in
App.vue
... use<router-view />
- Link via
router-link
- InApp.vue
look forv-list dense
in the template and add the following
<v-list dense>
<router-link :to="{ name: 'Dashboard' }">Dashboard</router-link>
<br />
<router-link :to="{ name: 'People' }">People</router-link>
</v-list>
<template>
<!-- Blatantly stolen from https://vuetifyjs.com/en/examples/layouts/sandbox -->
<v-app id="sandbox">
<v-navigation-drawer
v-model="primaryDrawer.model"
:permanent="primaryDrawer.type === 'permanent'"
:temporary="primaryDrawer.type === 'temporary'"
absolute
overflow
app
>
<div class="pa-3 text-xs-center teal white--text">
<div class="display-2 py-4">
Friends HQ
</div>
<p>Together, we are stronger</p>
</div>
<v-spacer></v-spacer>
<v-list dense>
<template v-for="(item, i) in items">
<v-divider dark v-if="item.divider" class="my-3" :key="i"></v-divider>
<v-list-tile :key="i" v-else :to="{ name: item.routeName }">
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title class="grey--text">
{{ item.text }}
</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</template>
</v-list>
</v-navigation-drawer>
<v-toolbar app absolute>
<v-toolbar-side-icon
v-if="primaryDrawer.type !== 'permanent'"
@click.stop="primaryDrawer.model = !primaryDrawer.model"
></v-toolbar-side-icon>
</v-toolbar>
<v-content>
<v-container fluid>
<v-layout align-center justify-center>
<v-flex xs10>
<v-card>
<v-card-text>
<v-layout row wrap>
<router-view></router-view>
</v-layout>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-content>
<v-footer :inset="true" app>
<span class="px-3">
© Looselytyped {{ new Date().getFullYear() }}
</span>
</v-footer>
</v-app>
</template>
<script>
export default {
data: () => ({
primaryDrawer: {
model: null,
type: "default (no property)"
},
items: [
{ icon: "dashboard", text: "Dashboard", routeName: "Dashboard" },
{ icon: "contacts", text: "Contacts", routeName: "People" },
{ divider: true },
{ icon: "notes", text: "Journal" }
]
})
};
</script>
- Modify
routes.js
to add a "child route"
{
path: "/people",
component: People,
children: [
{
path: "",
name: "People",
component: PeopleList
}
]
},
- Then create
views/People.vue
<template>
<v-container fluid>
<router-view />
</v-container>
</template>
- Fix
PeopleList
<template>
<v-container fluid>
<v-layout row>
<v-flex grow pa-1>
<v-card>
<v-list header>
<PersonItem
v-for="(friend, index) in friends"
:key="friend.id"
:friend="friend"
:last="index + 1 < friends.length"
@notify-parent="like"
></PersonItem>
</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>
-
There are no "form objects" backing it — You gotta do your own validation and such.
-
In
router.js
{
path: "/people",
component: People,
children: [
{
path: "",
name: "People",
component: PeopleList
},
{
path: "add",
name: "AddFriend",
component: AddFriend
}
]
},
- In
AddFriend.vue
<template>
<form @submit.prevent="submit">
<p v-if="errors.length">
<b>Please correct the following error(s):</b>
<ul>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<!-- <li v-for="error in errors">{{ error }}</li> -->
</ul>
</p>
<div>
<input type="text" v-model="firstName" />
</div>
<div>
<input type="text" v-model="lastName" />
</div>
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
firstName: "Raju",
lastName: "Gandhi",
errors: []
};
},
methods: {
submit() {
this.errors = [];
if (!this.firstName) {
this.errors.push("First name is required");
}
}
}
};
</script>
<style></style>
- Update
PeopleList.vue
<v-btn color="success" large :to="{ name: 'AddFriend' }">
Add Friend
</v-btn>
- To see the final form
git checkout 1a3b04cd5129f434b1df069a6bb9bd7fe3fce872