Hello neat designer passion fruit!

: Clippy, bash / zsh commands, CSS Color

Vue Basics (Worksheet)

Resources & Infos

Terminology

  • State Management => All data in one place = Store = Single-Source-of-Truth
  • SPA / MPA – Single Page Application / Multi Page Application

Vue root instance

new Vue({
   el: '#app',
   data: {
     // supplied data
     entries: datahere
   },
   computed: {
     // computed data, will be updated if data changes
   },
   methods: {
     // custom functions
   }
})
<!-- SCOPE: -->
<div id="app">
   <!-- accessible by vue -->
</div>
<div id="something">
   <!-- NOT accessible by vue -->
</div>

$mount

Alternatively to the el property, the app element can be declared with $mount(selector) method:

new Vue({
   data: { ... },
   ...
}).$mount('#app');

// Or, after instantiatig the Vue root instance
new Vue({...});
// ...
Vue.$mount('#app');

Directives (v-…)

Binding Data (v-bind & v-model)

One-Way (v-bind/:)

Text: {{ entries.title }
Attributes: prefix with v-bind:, omit {{ }} -> v-bind:src="entries.src"
Shortform: omit v-bind (but leave :):
:src="entries.src"

Bidirectional (v-model)

v-model="..."

Itterating (v-for)

v-for="dataPoint in dataSet"

<!-- entries = dataset, entry = data point (entries[n]) -->
<ul v-for="entry in entries">
  <li>entry.title</li>
</ul>

Note: Its recomended (and enforced by ESLint) to bind a key to for directives. If no unique key is available in the dataset, use the itteration index (The example below shows binding dynamic classnames aswell):

<ul 
  v-for="(entry, index) in entries"
  :key="index"
>
  <li
    class="item"
    :class="['item-'+index, 'item--'+entry.type]"
  >
  entry.title
  </li>
</ul>

Interaction (v-on/@)

<span v-on:click="someMethod(entry.id)">Click me!</span>
<!-- same (@ short form): -->
<span @click="someMethod(entry.id)">Click me!</span>

=> methods are defined in the Vue component within the object methods.

Show/hide (v-if & v-show)

v-if="boolMethod()" / v-show="boolMethod()"
Shows/hides content based on bool value of method.

  • v-if adds/removes element from dom. Use if element has dynamic content
  • v-show show/hides elmente via display:none. Use with static content.

Components

Components are own Vue instances, similar to the one inititialized via new Vue.

Vue.component('component-name', {
    props: ['list','of','available','props'],
    template: `<p>I am a <em>component</em></p>`,
    methods: {
      // Component methods hede
    }        
});

Passing on props (+ itterating)

<div
   v-for="items in list"
>
    <some-component 
        v-bind:items="items"
        v-bind:list="list"
    />
</div>

binds / passes on the props items and list which can be utilized in the component definition.

Vue Cli

Install : npm install -g @vue/cli

In project parent folder: vue create <project-name>

Basic component nesting

src/components/ComponentName.vue

  • Define component markup in <template>
  • export component name, list of used/received props etc. (eg computed) in <script>
  • define component (scoped attribute) or global styles in <style> (optionally use lang="" attribute)
<template>
  <div>
     <!-- component markup -->
  </div>
</template>

<script>
  export default {
    name: 'ComponentName',
    data(): {
      return {
        // custom component data here
      }
    },
    props: ['item', 'someProp']
  }
</script>

<style scoped>
  /* component only styles */
</style>

<style>
  /* global styles */
</style>

src/App.vue

  • Embend <Component /> in <template>, bind props (=> v-bind / : )
  • import component
  • export with components property
<template>
  <div>
    <ComponentName
       v-for="item in items"
       :item="item"
       :someProp="someOtherProp"
    />
  </div>
</template>

<script>
  import ComponentName from './components/ComponentName';

  export default {
    name: 'App',
    components: {
      ComponentName
    }
  }
</script>

Custom Events (child → parent)

1. Emitting the event from the child component, using
this.$emit("custom-event-name", { ... }) :

const BelovedChild = {
  template: `
    <button
      @click="callOut"
    />
  `,
  data() {
    return {
      msg: ''
    }
  }
  methods: {
    callOut() {
      this.$emit("calling-out", {
        message: this.msg
      })
    }
  }
}

2. Listening to the event (parent component) using @cutom-event-name="…"

<UnderstandingParent
  @calling-out="someResponse"
/>

Event Bus (A → BUS → B )

Concept: Send ($emit) an event and listen ($on) to it via an independant Vue() intance (= Event Bus). Use the created() lifecycle hook of the responding component for the event listener registration.

const EventBus = new Vue();
const BelovedChild = {
   ...,
   methods: {
    // Sending:
    callOut() {
      EventBus.$emit("calling-out", {
        message: this.msg
      })
    }
  }
}

const UnderstandingParent = {
  ...,
  methods: {
    someResponse(event) {
      // do something with event.message
    }
  }
  created() {
    // Listening:
    EventBus.$on("calling-out", event => someResponse(event));
  }
})

Vuex

  • Manages state via getters, mutations and actions.
  • getters, mutations and actions are objects containing methods.
  • the methods of mutations and actions take exact two parameters: state (mutations) respectively context (actions) and the payload as the second parameter.
  • the store (new Vuex.Store({state, getters, setters, actions}) is connected via the root Vue instance
  • actions use the comit('MUTATION_NAME', payload) method to comit to the mutation
  • the actions itself are called via the dispatch('actionName', payload') method
  • the store from the Vue root instance can be accessed with this.$store

~ Usage (eg. in store/index.js):
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({ modules: {...} });

const state = {
  basket: [...]
}
const getters = {
   getBasket() {
     return state.basket
   }
}
const mutations = {
   ADD_TO_BASKET (state, payload) {
     state.basket.push(payload)
   }
}

const actions = {
   addToBasket (context, payload) {
     context.commit('ADD_TO_BASKET', payload);
   }
}

const store = new Vuex.Store({state, getters, setters, actions});

const AddToBasketBtn = {
   ...
   methods: {
     addItemToBasket(item) {
      this.$store.dispatch('addToBasket', item)
     } 
   }
}

mapGetters (vuex)

using getters can end up in a logn list of computed property functions, simply returning a store item via the getter, e.g

computed: {
  cartItems() {
    return this.$store.getters.cartItems;
  },
  cartTotal() {
    return this.$store.getters.cartTotal;
  },
  // so on / so forth ...
}

To simplify this vuex has a mapper function, mapGetters (needs to be imported [→ import { mapGetters } from 'vuex'] but is part of the vuex package, so no extra npm for this).
It takes an array of the getter names.
This code results to the same as the one above

computed: mapGetters([
    'cartItems',
    'cartTotal'
])

The problem with the code above is, that further computed properties (maybe involving calculations) can not be simply appended. To solve this, spread (...) the mapGetters into an object like this

computed: {
  ...mapGetters([
    'cartItems',
    'cartTotal'
  ]),
  cartTaxes() {
    // some calculations maybe involving the cardTotal...
  }
}

mapActions

same as mapGetters, but for actions (mapActions(['a','b','...'])).
The payload of the event it being transmitted to the named Action (eg @click="addCartItem(cartItems)" => mapActions(['addCardItem']) dispatches action addCardItem with the payload cardItem.)

Note: It’s also possible to map a function name to another by using an object instead of an array and the key/value notation:

...mapActions({
   srcFunctionName: 'mappedFunctionName'
})

For more see: A complete guide to mapping in Vuex

conventions

mutation types (convention)

for maintainability and scalability reasons it is recommended to store the mutations per module group into a separate js file.

For example consider the mutation UPDATE_CART_ITEMS within a cart module.

  1. Outsorce it to a file called mutation-types.js within the module folder (/cart/) : export const UPDATE_CART_ITEMS = 'UPDATE_CART_ITEMS' (repeat for all other cart mutation types)
  2. Import it to the index.js, where the mutations are situated: import * as types from './mutation-types';
  3. Replace now the mutation UPDATE_CART_ITEMS (state, payload) {} with [types.UPDATE_CART_ITEMS] (state, payload) {} as well as the occurence of UPDATE_CART_ITEMS in the actions (without [], just types.UPDATE_CART_ITEMS nota bene.

outsource store constants (state, mutations, actions, getters)

… for further maintainability.

Example with actions:

  1. outsource const actions = {...} to module_name/actions.js and export it (export const actions = { ... }).
  2. import it (destructured) again in the module_name/index.js like import { actions } from './actions'. Finito.

Routing

Install
npm install --save vue-router

Implement

1. Define Routes

// routes.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import myComponent from './components/myComponent'
// more imports ...

Vue.use(VueRouter)

const routes = [
    {
        path: '/mypath',
        component: myComponent
    }, 
    // more route objects ...
]

export const router = new VueRouter({
    mode: 'history', // nice urls (otherwise #/... )
    routes
})

2. Import/use router

// main.js
import Vue from 'vue'
import App from './App.vue'
import { router } from './routes' // <=
// more imports ...

new Vue({
  router, // <=
  // store, render: ...
}).$mount('#app');

3. Place rout links (<router-link to="...">) and component placeholders (<router-view />)

// app.vue
<template>
  <div>
    ...
      // Link to route
      // Of course this would be within its own component with nice markup ...
      <router-link to="/mypath" class="pseudo-nav-link">
         Linktext
      </router-link>
    ...
    <router-view /> // injects myComponent (defined in routes var in routes.js)
    ...
  </div>
</div>

dynamic routes

Use : folowed by custom attr name.

path: "product/:id",
component: productItem,
props: true // optionally send attrs drirectly to component props

For dynamic router-links bind use :to= (binded) like
:to="`/products/${productItem.id}`"

catch-all route

Add * path to end of routes

path: "*",
component: ...

children property

Loads component in component (in place of <router-view> tag) if path matches.

example for path books/list

const appRoutes = [
  {
    path: '/books',
    component: '../layouts/BooksLayout.vue',
    children: [{
        path: 'list',
        component: ''../pages/BooksListPage.vue''
    }]
]

Important: No beginnign slash for child path!

$router

Like $store from vuex, there is a globaly accessible $router variable, holding infos and methods like …

  • $router.go(-1) : history -1
  • $router.push('/cart') : navigates to /cart route

Watchers

Watches when value changes

comupted {
   token() {
      return ...
   } 
},
watch: {
  token(newValue, oldValue) {
        if (this.token != 'something') {
            ...
        }
  }

Navigation Guards

router.js

const routes = {
  path: '/login',
    component: Signin,
    // Example: if logged in, redirect '/login' to '/'
    beforeEnter: (to, from, next) => {
      const token = localStorage.getItem('token');
      // dynamic route params are stored in to.params
      if (token) {
        next('/')
      } else {
        next();
      }
    }
  },
}

const router = new VueRouter({...})

// Global route guard
router.beforeEach((to, from, next) => {
    const token = localStorage.getItem('token');
    if (!token && to.path !== '/login') {
        next('/login');
    } else {
        next();
    }
});

export default router;

Module Lazy Loading

Use an import function for the component as compontent whitin the routes definition:

const appRouts = [
  {
    path: '/',
    component: () => import('../path/to/Component.vue')
  }
]

Important: Use file extension suffix (.vue)