Table of Contents generated with DocToc
- Visual Studio Code Installed extensions
- 1. Dinamically bind data using
v-bind
directive - 2. React to events with v-on directive
- 3. React to double click events with v-on directive
- 4. The Event Object
- 5. KeyBoard Events
- 6. Two Way Data Binding
- 7. Event and Key Modifiers
- 8. Conditional Output with v-if
- 9. Looping with v-for
- 10. The Vue CLI 3
- 10.1. VUE CLI Service, Plugins
- 11. Components and Vue files
- 12. The data() function
- 13. Nesting components
- 14. Scoped CSS - not as efficient
- 15. Passing Data with Props
- 16. Custom Events
- 17. Life-cycle Hooks
- 18. Making Requests with Axious
- 19. Filters
- 20. Computed Properties(custom search box)
- 21. What is the Vue Router
- 22. Setting up Routes
- 23. Router Links
- 24. Route Parameters
- 25. Watching the $route Object
- 26. More on Router Links
- 27. Programmatically Redirecting Users
- 28. Hash vs History Mode
- 29. Styling Active Links
- 30. Project Preview & Setup
- 31. Project Structure
- 32. Material Design
- 33. Navbar Component
- 34. Index Component
- 35. Deleting (local) Data
- 36. Introduction to Firebase
- 37. Setting up Firestore
- 38. Installing Firebase
- 39. Retrieving Firestore Data
- 40. Deleting Firestore Data
- 41. Add Smoothie Component
- 42. Adding Ingredients
- 43. Outputting Ingredients
- 45. Saving Records to Firestore
- 46. Deleting Ingredients
- 47. Edit Smoothie Route
- 48. Firestore Queries
- 49. Edit Smoothie Form
- 50. Updating Firestore Records
- 51. Deploying to Firebase
- 52. Chat Project Overview & Setup
- 53. Passing
props
via Routes - 54. Route Guard
- 55. Add Firestore Docs in non-existent Collection
- 56. Real time Events (Event Listeners)
- 57. Formating time with Moment.js
- 58. Auto scrolling for a Real Time Updating Page
- 59. Google Maps Api
- 60. Creating a new Map
- 61. Firebase Auth
- 62. Form Validations
- 63. Route Guarding (auth)
- Check auth status with onAuthStateChanged
- 64. Google Map Marker
- 65. Real time comments update
- 66. Firebase Cloud Functions
- 67. Firebase Rules
- 68. Deploy to Firebase
- 69. Instant Prototypeing
- 70. Use VUE CLI 3 to build App, Libs or Web Components
- 71. Use VUE CLI GUI
- 72. Redux Store in Vue
Vue:
-
front-end JS framework used for web SPAs or widgets
-
very small compared to Angular or React
-
for this project you need to replace the indexFake and initFake with real configs
VS Live Share
npm Intellisense
Debugger For Chrome
TS Lint
Auto Import
Sass
Npm
Live Server, Monokai++, Vetur
`<a v-bind:href="url">Youtube</a>`
or shorter:
`<a v-bind:href="url">Youtube</a>`
An event object is created;
<p>I earn {{ wage }} pounds per hour</p>
<button v-on:click="wage++">Increase wage by 1$ </button>
<button v-on:click="changeWage(-1)">Decrease wage by 1$ </button>
<button v-on:dblclick="changeWage(-4)">Decrease wage by 4$</button>
<button @click="logEvent">Log Event info</button>
<div class="canvas" @mouseMove="logCoords">{{ coords.x }},{{ coords.y }}</div>
new Vue({
el: '#app',
data: {
},
methods: {
logEvent(e){
console.log(e);
},
logCoords(e){
this.coords.x = e.offsetX;
this.coords.y = e.offsetY;
console.log(screenX, screenY);
}
}
});
<p>My name is {{ name }}</p>
<input type="text" @keyup="updateName">
// ...
updateName(e) {
// console.log(e.target.value);
this.name = e.target.value;
}
<p>Two way data-binding for title {{ title }}:</p>
<input type="text" v-model="title">
Modify how we listen to click or key events using the .
like this:
<button @click.alt="eventModifiers">Test alt-click event key modifier</button>
<button @click.alt="eventModifiers">Test shift-click event key modifier</button>
runs eventModifiers method if Alt key, respectivly Shift is pressed when mouse clicking.
<a href="google.com" @click.prevent="eventModifiers">Google</a>
prevents default behaviour.
v-if
and v-else-if
directives
<p v-if="!loggedIn"> v-if show 'Login' for logged in {{ showName }}:
<button @click="login">Login</button>
</p>
<p v-else-if="showName"> v-else-if show 'Hi, {{ name }}' for knowned loggedin user: {{ showName }}
<button @click="logout">Logout </button>
</p>
<div v-for="(ninja, index) in ninjas">
<p>
{{ index }}.{{ ninja.name }} is {{ ninja.age }} years old and has a {{ ninja.belt }}
</p>
</div>
where ninjas is an array of objects.
for the old CLI:
npm i -g vue-cli
vue init webpack-simple vueproject
...
For the new CLI 3:
npm uninstall -g vue-cli
npm i -g @vue/cli
install cli package from @vue scope
vue create vueproject
-
allows change of project config using plugins;
-
graphical User Interface
Build Setup
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
For detailed explanation on how things work, consult the docs for vue-loader.
vue-cli-service serve
or npm run serve
to preview the app
npm run lint
to log the errors
Add plugins running: vue add vuetify
- this will add the plugin cli-plugin-vuetify
into package.json, dependencies file.
new Vue({
el: '#app',
render: h => h(App)
})
because we want this instance to control the #app element.
Each Vue object/instance has a corresponding component (e.g. App.vue - the main component) with a template containing one main div(
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
The data() function from each component is required to protect the data and make it unique (if if had been just an object,
data wouldn't be protected because it would be shared between components).
import Nav from './Navbar';
// ..
export default {
name: 'app',
components: {
Nav,
// ..
},
Vue.js automatically applies the <style>
from a component side-wide( to all components imported into main component), not oly to that component.
- for scopeded CSS (available just for that component) we have to apply use scoped keyword
<style scoped>
,
but preferably, don't use it because is not so efficient - but rather add classes to elements.
Ctrl+Shift+R - hard refresh page to see changes after this CSS change.
is the @Input from Angular - here we bind data using :
in front of the property added in the component in the props array:
<OnlineFriends :friends="friends"/>
also add :
export default {
name: 'OnlineFriends',
props: ['friends'],
// ..
}
where friends is data inside the main component.
Delete a friend from data.
We use the $emit
method to emit custom events (here delete) from the child component to main component:
methods: {
unfriend(friend) {
this.$emit('delete', { name : friend });
}
}
Parent component listens at the emitted event in the component's call:
<AllFriends :friends="friends" @delete="deleteFriend"/>
(use v-on:
or @
)
and has the deleteFriends method:
methods: {
deleteFriend(payload) {
this.friends = this.friends.filter(
friend => friend.name !== payload.name
)
}
// ..
-
beforeCreate
-
created ( load any data here)
-
beforeMount (before compoment loads into the DOM)
-
mounted
-
beforeUpdated
-
updated
-
beforeDestroy
-
destroyed (here any required cleanup after the component has been destroyed)
npm i axios
console.log('created hook');
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(response => {
console.log(response);
this.posts = response.data;
})
.catch(error => console.log(error))
Add the snippet filter in main.js
Vue.filter('snippet', val => {
if(!val || typeof(val) != 'string') {
return ' ';
}
val = val.slice(0, 50);
return val + '...';
});
apply the filter(pipe from Angular) using the pipe:
<p>{{ post.body | snippet }}</p>
The computed
prop looks at some data and manipulates that data
Add the computed
property in component's export:
export default {
name: 'Blogs',
data() {
return {
// ..
posts: [],
searchTerm: '',
// ..
}
},
computed: {
filterPosts() {
return this.posts.filter(
posts => posts.title.match(this.searchTerm)
);
}
},
// ..
Iterate through the return of computed object (filterPosts() object), instead of all posts:
<div v-for="post in filterPosts" :key="post.id">
<h3>{{ post.title }}</h3>
<p>{{ post.body | snippet }}</p>
</div>
vue init webpack vuerouting
with the old CLI
vue create vuerouting
to generate a new project with router
in main component we have : <router-view/>
tag wich will load
all components added inside the Router object from index.js.
Vue.use(Router)
export default new Router({
routes: [
{
path: '/home',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/profile/:user_id',
name: 'View Profile',
component: ViewProfile
}
]
// ...
access the about page: http://localhost:8080/#/about
We do not use <a href=''>
, instead we use <router-link to=''>
or <router-link :to={}>
:
<li><router-link to="/">Home</router-link></li>
<li><router-link :to="{ name: 'About' }">About</router-link></li>
read the param from route:
data() {
return {
userId: this.$route.params.user_id
}
// ..
methods: {
updateId() {
this.userId = this.$route.params.user_id;
}
},
watch: {
$route: 'updateId'
}
when we have the watch property, the updateId will run and update data with the new route parameter;
<li v-for="(id,index) in userIds" :key="index">
<router-link :to="{name: 'ViewProfile', params: { user_id: id}}">
<span>User Profile {{ id }}</span>
</router-link>
</li>
this.$router
keeps track of all routes, versus: this.$route
that refers to the current route;
goHome() {
this.$router.push({name: 'Home'});
},
goBack() {
this.$router.go(-1);
},
goForward() {
this.$router.go(1);
}
<div>
<button @click="goHome">
Redirect to home
</button>
<button @click="goBack">
Go back
</button>
<button @click="goForward">
Go forward
</button>
</div>
The #
symbol in links is required to be there to prevent a new call to the server, when accessing a new page.
On local server, to hide the hash, we set the mode
property of the browser to 'history':
export default new Router({
mode: 'history',
routes: [
// ..
.router-link-exact-active {
color: blue
}
Use Firestore = a real time noSQL database provided by Firebase.
use Material CSS library: (https://materializecss.com/getting-started.html)[https://materializecss.com/getting-started.html]
<div class="navbar">
<nav class="nav-extended indigo darken-2">
<div class="nav-content">
<router-link to="">
<span class="nav-title">Ninja Smooties</span>
</router-link>
<a href="" class="btn-floating btn-small halfway-fab pink">
<router-link to=""><i class="material-icons">add</i></router-link>
</a>
</div>
</nav>
</div>
<div class="index container">
<div class="card" v-for="smootie in smooties" :key="smootie.id">
<div class="card-content">
<h2 class="indigo-text"> {{ smootie.title }}</h2>
<ul class="ingredients">
<li v-for="(ing, index) in smootie.ingredients" :key="index">
<span class="chip">
{{ ing }}
</span>
</li>
</ul>
<i class="material-icons delete" @click="deleteItem(smootie.id)">delete</i>
</div>
</div>
</div>
deleteItem(id) {
this.smooties = this.smooties.filter(
smootie => smootie.id !== id
)
Use Firestore :
-
a real time noSQL database provided by Firebase
-
authentication
-
app deployment
Google Firebase -> Go to Dashboard
npm i firebase
Project Overview and click on: </> to copy the config object
- files initFake.js(src/firebase path) and indexFake.html should be replaced with index.html and init.js (currently mentioned in .gitignore)
import firebase from 'firebase';
//initialize Firebase
var config = {
apiKey: "YOUR_API_KEY",
authDomain: "ninja-smooties-97572.firebaseapp.com",
databaseURL: "https://ninja-smooties-97572.firebaseio.com",
projectId: "ninja-smooties-97572",
storageBucket: "ninja-smooties-97572.appspot.com",
messagingSenderId: "291122468448"
};
const firebaseApp = firebase.initializeApp(config);
// firebaseApp.firestore().settings({timestampsInSnapshot: true});
export default firebaseApp.firestore();
and in the created life cicle hook:
created() {
db.collection('sooties').get()
.then(snapshot => {
snapshot.forEach(doc => {
// console.log(doc.data(), doc.id);
let smootie = doc.data()
smootie.id = doc.id
this.sooties.push(smootie)
})
}).catch(err => console.log(err))
},
deleteItem(id) {
db.collection('sooties').doc(id).delete()
.then(() => {
this.sooties = this.sooties.filter(
smootie => smootie.id !== id
)
})
}
Prevent the defalt behaviour of the submit event(refresh page), using the prevent modifier:
<form @submit.prevent="AddSmootie">
<input type="text" for="title" name="title" v-model="title" >
After entering an ingredient, and pressing tab the defalt event of passing to the following field is prevented
and addIng method is executed:
<div class="field add-ingredient">
<label for="text" name="add-ingredient">
Add an ingredient:
</label>
<input type="text" for="add-ingredient" @keydown.tab.prevent="addIng" v-model="another">
</div>
<div v-for="(ing, index) in ingredients" :key="index">
<label for="ingredient">
Ingredient:
</label>
<input type="text" v-model="ingredients[index]" />
</div>
npm i slugify --save
to import a llibrary to generate slugs
AddSmootie() {
if (this.title) {
this.feedback = null
this.slug = slugify(this.title, {
replacement: '-',
remove: /[$*_+¬.()'"!\-:@]/g,
lower:true
})
db.collection('sooties').add({
title: this.title,
slug: this.slug,
ingredients: this.ingredients
}).then(
() => this.$router.push({ name: 'Index' })
).catch( err => console.log(err))
} else {
feedback = ' You must enter a new title'
}
},
Delete an ingredient localy, before saving into database:
<div class="field" v-for="(ing, index) in ingredients" :key="index">
<label for="ingredient">
Ingredient
</label>
<input type="text" v-model="ingredients[index]" />
<i class="material-icons delete" @click="deleteIng(ing)">delete</i>
</div>
deleteIng(ing) {
this.ingredients = this.ingredients.filter( ingredient => ingredient !== ing)
}
{
path: '/edit-smootie/:slug',
name: 'EditSmootie',
component: EditSmootie
}
collection is like a table in database, doc is like an entry in the table
db.collection.doc.update // similar to update a row from tb
db.collection.doc.add // add a row in table
db.collection.doc.push // add a row in table
db.collection.where('row', 'comparison operator', 'value for the entry to be returned').get // is a select whit where clause
db.collection.where('row', 'comparison operator', 'value for the entry to be returned').onSnapshot to use the returned snapsot with docChanges()
db.collection.orderBy.get //select with order clause
// ..
is like the add form, but this time we specify input values using v-model directive with smootie
object obtained in the created()
lifecycle hook.
Done like this, but is not correct to get the doc id from slug if slug is not unique:
created() {
let ref = db.collection('sooties').where('slug', '==', this.$route.params.slug);
ref.get().then(snapshot => {
return snapshot.forEach(doc => {
this.smootie = doc.data();
this.smootie.id = doc.id;
})
}).catch(err => console.log(err));
but rather use the id, or even don't make a new http request to get data, use local data!:
let ref = db.collection('sooties').doc(this.id);
ref.get().then(doc => {
this.smootie = doc.data();
this.smootie.id = doc.data().id;
console.log(this.smootie);
}).catch(err => console.log(err));
console.log(ref);
db.collection('sooties').doc(this.smootie.id).update({
// ..
access firebase prj console: [https://console.firebase.google.com/project/myprojid/overview]
go to Hosting section
npm i -g firebase-tools --save
firebase login
firebase init
and here:
-
navigate with the arrow to firestore option and hosting option and select each by pressing space
-
press enter
-
select the defalt project ( your prj name)
-
select as public folder the dist folder
-select spa
npm run build
?? have to build the project before deploy, otherwise you'll have an empty index filed - deployed project:
at https://myprojid.firebaseapp.com/
firebase deploy
Use REAL-TIME DATA update TECHIQUE
vue init webpack vue-chat
npm start
// in the redirecting component:
this.$router.push({ name: 'Chat', params: { name: this.name }});
// ... and in the Chat component:
export default {
name: 'Chat',
props: ['name'],
// ...
// and in router:
{
path: '/chat',
name: 'Chat',
component: Chat,
props: true
},
// ...
Handle the case where the route does not exist - add a beforeEnter function value:
{
path: '/chat',
name: 'Chat',
component: Chat,
props: true,
beforeEnter: (to, from, next) => {
if (to.params.name) {
next()
}
else {
next({name: 'Welcome'})
}
}
}
if the collection doesn't exist will automatically create it:
db.collection('message').add({
content: this.newMessage,
name: this.name,
timestamp: Date.now()
}).catch(
err => console.log(err)
)
In chat's page we should see all the messages (messages = []) of the conversation
Listen for the messages when the component is created (created())
Each entered message is added in firebase, so we can use a snapshot of the messages collection,
and get only those added using the docChanges method:
created() {
let ref = db.collection('messages').orderBy('timestamp')
ref.onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if (change.type == 'added') {
let doc = change.doc
this.messages.push({
id: doc.id,
name: doc.data().name,
content: doc.data().content,
timestamp: doc.data().timestamp,
})
}
});
}
)
},
npm i moment --save
timestamp: moment(doc.data().timestamp).format('lll'),
npm i vue-chat-scroll
.messages {
text-align: left;
max-height: 300px;
overflow: auto;
}
.messages::-webkit-scrollbar {
width: 3px;
}
.messages::-webkit-scrollbar-track{
width: #ddd;
}
.messages::-webkit-scrollbar-thumb {
width: #aaa;
}
In main.js import the VueChatScroll class from the vue-chat-scroll
library
and plugin this class(plugin = package) like this:
Vue.use(VueChatScroll)
to enable the Maps JavaScript Api for your project.
Then: Credentials -> Credentials Manager -> Create Credentials -> and copy the API key for later usage.
Go to: (https://cloud.google.com/maps-platform/)[https://cloud.google.com/maps-platform/]
Copy the <script>
tag from the buttom of the page:
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async defer></script>
and place it in the index.
We need to insert the map into the DOM, so we cannot create it in the created() lifecle hook of the component, but rather in the mounted()
hook, when the component has already mounted the DOM.
The Server( Firebase ) will interact with the Vue App:
-
Firebase Auth generates an id for each added user
-
Firestore DB will have a geolocation entry for each user(identified by id)
-
Google project console > Authentication > Get a sign-up method > email & pass method
(for example)> Enable
For the moment we will handle the validation on Vue App side, but is better to do it using
Firebase Cloud.
- create a reference to the document (entry in the users table=collection) of interest (in this case with the provided slug):
let usersRef = db.collection('users').doc(this.slug)
- use Google's Firebase Auth for autenticating users (provides field validation also):
firebase.auth().createUserWithEmailAndPassword(this.email, this.password)
.catch( err => {
this.feedback = err.message;
})
Calling createUserWithEmailAndPassword
of Firebase Auth service will receive the credentials object (cred
):
firebase.auth().createUserWithEmailAndPassword(this.email, this.password)
.then(
cred => {
usersRef.set({
alias: this.alias,
geolocation: null,
id: cred.user.uid
})
})
Get CurrentUser info : firebase.auth().currentUser
Some rendering componets need firebase response and that's why we need to delay the rendering of the app until the firebase data loads:
let app = null;
//wait for firebase auth before creating the app:
firebase.auth().onAuthStateChanged(() => {
//init app if not already created:
if (!app) {
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
}
})
Logout the user using Firebase Auth method:
logout() {
firebase.auth().signOut().then(
() => {
this.$router.push({name: 'Login'})
}
)
}
Login the user :
firebase.auth().signInWithEmailAndPassword(this.email, this.password)
Use browser's geolocation info to render the map using coordinates provided by browser:
navigator.geolocation.getCurrentPosition(pos => {
this.lat = pos.coords.latitude
this.lng = pos.coords.longitude
this.renderMap()
},
(err) => {
console.log(err)
this.renderMap()
},
{ maximumAge: 60000, timeout: 3000})
Protect page from non-authenticated users, by adding to component's route:
meta: {
requiresAuth: true
}
Before each route coming from
and going to
a route with requiresAuth
meta
property, the next
method is called in different ways, accordig to user's auth status:
-
with no params, will proceed to route
-
with redirecting component as parameter
router.beforeEach((to, from, next) => {
//check to see if route requires auth
if (to.matched.some(rec => rec.meta.requiresAuth)) {
// check auth state of user
let user = firebase.auth().currentUser
if (user) {
// user signed in proceed to route
next()
} else {
// no user signed in, redirect to home
next({name: 'Login'})
}
} else {
next()
}
})
created() {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
this.user = user
} else {
this.user = null
}
})
},
Create a new google.maps.Marker object and use it to display user's current location:
db.collection('users').get().then(
users => {
users.docs.forEach(doc =>
{
let data = doc.data()
if (data.geolocation) {
let marker = new google.maps.Marker({
position: {
lat: data.geolocation.lat,
lng: data.geolocation.lng
},
map
})
// add click event to marker:
marker.addListener('click', () => {
console.log(doc.id)
})
}
})
}
)
Use the onSnapshot and docChanges methods:
db.collection('comments').where('to', '==', this.$route.params.id)
.onSnapshot((snapshot) =>
{
snapshot.docChanges().forEach(
change => {
if (change.type == 'added') {
this.comments.unshift({
from: change.doc.data().from,
content: change.doc.data().content
})
console.log(this.comments);
}
}
)
}
)
- can be used to create configurations to take care of all the server management ( data processing functions that we use inside our app).
npm install firebase-functions@latest firebase-admin@latest --save
npm install -g firebase-tools
firebase login
firebase init functions
write the functions into functions/index.js
cd ./functions
npm i googleapis
firebase deploy --only functions
to deploy the functions to Firestore
You can restrict database access, changing the Rules
section or the firestore.rules file
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write: if requests.auth.uid !=null;
}
}
}
deploy project to propagate rules changes to Firebase.
Remember to use VUE CLI 3 and you will build the app simple:
nmp run build
( this is not executing the build.js file like in the old project created with the old VUE CLI)
You can access your firebase app at https://yourprjname.firebaseapp.com after deploing:
firebase deploy --only hosting
npm i -g @vue/cli-service-global
this adds the serve vue cli and we can run:
vue serve online.vue
inside's online.vue component to instantly preview the component
npm i -g @vue/cli-service-global
we build the web componet into a web component(online-status):
vue build online.vue --target wc --name online-status
vue ui
and in the opened [http://localhost:8000/project/select]:
import your projects
or create a new one with the default presets ... cd into prj folder and run npm serve