A Full Day of Vue.js

05. Routing & Vue Router

Vue Router

Vue

Vuex

Vue Test Utils

 Routing

Routing often refers to splitting an applications UI  based on rules derived from the browser URL.

 Routing

https://www.twitter.com/

 Routing

https://www.twitter.com/{userName}
Dynamic route

 Routing

https://www.twitter.com/{userName}
protocol
hostname
pathname

 Routing

Routing allows us to...

- Refresh a page and keep our location in an app

- Bookmark a URL and share it with others

- Maintain browser back/forward history functionality

 Routing

Two main ways to achieve routing in a web application

Server Side Routing

Client Side Routing

The client (i.e. the browser) makes a request to the server on every URL change.

The client only makes a request to the server upon initial page-load.

First Meaningful Paint

Consistent SEO performance

Faster routing after initial load

Smooth transitions and changes

2

1

 Routing

Single-page applications

(SPAs)

Client side apps are often labelled as...

/charizard

/blastoise

/venusaur

 Routing

We'll build the Pokemon Routing app with:

1. Custom client-side routing

2. Vue Router

 Routing - Custom Client-Side Router

Starting Point

App

CharizardCard

 Routing - Custom Client-Side Router

Starting Point

<template>
  <div class="card card--charizard has-text-weight-bold has-text-white">
      <div class="card-image">
        <div class="card-image-container">
          <img src="../../../static/charizard.png"/>
        </div>
      </div>
      <div class="card-content has-text-centered">
        <div class="main">
          <div class="title has-text-white">Charizard</div>
          <div class="hp">hp 78</div>
        </div>
        <div class="stats columns is-mobile">
          <div class="column">🔥<br>
            <span class="tag is-warning">Type</span>
          </div>
          <div class="column center-column">199 lbs<br>
            <span class="tag is-warning">Weight</span>
          </div>
          <div class="column">1.7 m <br>
            <span class="tag is-warning">Height</span>
          </div>
        </div>
      </div>
    </div>
</template>

<script>
export default {
  name: "CharizardCard",
};
</script>
<template>
  <div class="container">
    <div class="pokemon">
      <PokemonCard />
    </div>
  </div>
</template>

<script>
import CharizardCard from "./components/CharizardCard";

export default {
  name: "App",
  components: {
    PokemonCard: CharizardCard
  }
};
</script>

<style lang="css" src="../styles.css">
// Styles from stylesheet
</style>
CharizardCard.vue
App.vue

 Routing - Custom Client-Side Router

src/
components/
BlastoiseCard.vue
CharizardCard.vue
VenusaurCard.vue
App.vue
main.js

 Routing - Custom Client-Side Router

CharizardCard.vue
VenusaurCard.vue
BlastoiseCard.vue
App.vue

 Routing - Custom Client-Side Router

src/
router/
RouterLink.vue
RouterView.vue
routes.js
App.vue
main.js
components/

 Routing - Custom Client-Side Router

routes.js

Responsible in mapping components to respective URL pathnames.

 Routing - Custom Client-Side Router

import BlastoiseCard from "../components/BlastoiseCard";
import CharizardCard from "../components/CharizardCard";
import VenusaurCard from "../components/VenusaurCard";

export default [
  { path: "/", component: CharizardCard },
  { path: "/charizard", component: CharizardCard },
  { path: "/blastoise", component: BlastoiseCard },
  { path: "/venusaur", component: VenusaurCard }
];
src/router/routes.js

 Routing - Custom Client-Side Router

RouterView.vue

Component responsible in rendering another component based on the app's location

 Routing - Custom Client-Side Router

RouterView.vue
<template>
  <div class="pokemon">
    <component :is="currentView"></component>
  </div>
</template>

<script>
import routes from './routes';

export default {
  name: "RouterView",
  data() {
    return {
      currentView: {}
    }
  },
  created() {
    if (!this.getRouteObject()) {
      this.currentView = {
        template: `
          <h3 class="subtitle has-text-white">
            Sorry, we couldn't find that Pokémon :(.
          </h3>
        `
      };
    } else {
      this.currentView = this.getRouteObject().component;
    }
  },
  methods: {
    getRouteObject() {
      return routes.find(
        route => route.path === window.location.pathname
      );
    }
  }
};
</script>

 Routing - Custom Client-Side Router

<template>
  <div class="container">
    <div class="pokemon">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
import RouterView from "./router/RouterView";

export default {
  name: "App",
  components: {
    'router-view': RouterView
  }
};
</script>

<style lang="css" src="../styles.css">
// Styles from stylesheet
</style>
App.vue

 Routing - Custom Client-Side Router

/charizard
/blastoise
/venusaur
/pikachu

 Routing - Custom Client-Side Router

<a href="?"></a>

 Routing - Custom Client-Side Router

RouterLink.vue

Component that allows a change in browser location without making a web request

 Routing - Custom Client-Side Router

<template>
  <a @click="navigate" :href="to">{{ to }}</a>
</template>

<script>
export default {
  name: "RouterLink",
  props: {
    to: {
      type: String,
      required: true
    }
  },
  methods: {
    navigate(evt) {
      evt.preventDefault();
      window.history.pushState(null, null, this.to);
    }
  }
};
</script>
RouterLink.vue

 Routing - Custom Client-Side Router

 Routing - Custom Client-Side Router

RouterLink

RouterView

created()

Event Bus

 Routing - Custom Client-Side Router

import Vue from "vue";
export default new Vue();
event-bus.js

 Routing - Custom Client-Side Router

<template>
  <a @click="navigate" :href="to">{{ to }}</a>
</template>

<script>
import EventBus from './event-bus';

export default {
  name: "RouterLink",
  props: {
    to: {
      type: String,
      required: true
    }
  },
  methods: {
    navigate(evt) {
      evt.preventDefault();
      window.history.pushState(null, null, this.to);
      EventBus.$emit('navigate');
    }
  }
};
</script>
RouterLink.vue

 Routing - Custom Client-Side Router

RouterView.vue
<template>...</template>

<script>
import routes from './routes';
import EventBus from '../event-bus';

export default {
  name: "RouterView",
  data() {
    return {
      currentView: {}
    }
  },
  created() {
    // Get correct component upon page load
    ...

    // Get correct component upon redirect
    EventBus.$on('navigate', () => {
      this.currentView = this.getRouteObject().component;
    });
  },
  methods: {
    getRouteObject() {
      return routes.find(
        route => route.path === window.location.pathname
      );
    }
  },
};
</script>

 Routing - Custom Client-Side Router

<template>
  <div class="container">
    <div class="pokemon">
      <router-view></router-view>

      <div class="pokemon-links has-text-centered">
        <router-link to="/charizard"></router-link>
        <router-link to="/blastoise"></router-link>
        <router-link to="/venusaur"></router-link>
      </div>
    </div>
  </div>
</template>

<script>
import RouterView from "./router/RouterView";
import RouterLink from "./router/RouterLink";

export default {
  name: "App",
  components: {
    'router-view': RouterView,
    'router-link': RouterLink
  }
};
</script>

<style lang="css" src="../styles.css">
// Styles from stylesheet
</style>
App.vue

 Routing - Custom Client-Side Router

popstate

Event to handle browser navigation events

 Routing - Custom Client-Side Router

<template>
  ...
</template>

<script>
import RouterView from "./router/RouterView";
import RouterLink from "./router/RouterLink";
import EventBus from './event-bus';

export default {
  name: "App",
  created() {
    window.addEventListener('popstate', () => {
      EventBus.$emit('navigate');
   });
  },
  components: {
    'router-view': RouterView,
    'router-link': RouterLink
  }
};
</script>

<style lang="css" src="../styles.css">
// Styles from stylesheet
</style>
App.vue

 Routing - Custom Client-Side Router

- routes

Responsible in mapping components to respective url pathnames.

- router-view
- router-link

Component responsible in rendering another specified component based on the app’s location.

Component that allows the user to change the location of the browser without making a web request

Same pieces that Vue Router provides!

 Routing - Vue Router

Vue Router is the official router of Vue. It deeply integrates with Vue to make building Single Page applications a breeze.

 Routing - Vue Router

npm install vue-router --save

 Routing - Vue Router

Vue.use(VueRouter);
import Vue from 'vue';
import VueRouter from 'vue-router';

 Routing - Vue Router

src/
router/
RouterLink.vue
RouterView.vue
routes.js
App.vue
main.js
components/
router.js

 Routing - Vue Router

import Vue from 'vue';
import VueRouter from 'vue-router';
import BlastoiseCard from './components/BlastoiseCard';
import CharizardCard from './components/CharizardCard';
import VenusaurCard from './components/VenusaurCard';

Vue.use(VueRouter);

const routes = [
  {path: '/', component: CharizardCard},
  {path: '/charizard', component: CharizardCard},
  {path: '/blastoise', component: BlastoiseCard},
  {path: '/venusaur', component: VenusaurCard},
  {
    path: '*',
    component: {
      template: `
        <h3 class="subtitle has-text-white">
          Sorry. We couldn't find that Pokémon :(.
        </h3>
      `
    }
  }
];

export default new VueRouter({
  mode: 'history',
  routes
});
router.js

 Routing - Vue Router

https://localhost:8080/#/blastoise

Hash mode URLs

Never sent to the server

 Routing - Vue Router

https://localhost:8080/blastoise

History mode URLs

 Routing - Vue Router

import Vue from "vue";
import App from "./App";
import router from "./router";

new Vue({
  el: "#app",
  router,
  render: h => h(App)
});
main.js

 Routing - Vue Router

<template>
  <div class="container">
    <div class="pokemon">
      <router-view></router-view>

      <div class="pokemon-links has-text-centered">
        <router-link to="/charizard">/charizard</router-link>
        <router-link to="/blastoise">/blastoise</router-link>
        <router-link to="/venusaur">/venusaur</router-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "App"
};
</script>

<style lang="css" src="../styles.css">
// Styles from stylesheet
</style>
App.vue

 Routing - Vue Router

/charizard
/blastoise
/venusaur
/pikachu

 Routing - Vue Router

Vue Router > simple custom router

- Well Tested

- Consistency between different browsers

- Dynamic route matching

- Nested routes

- Navigation guards

...

 Routing - Vue Router - Dynamic Route Matching

Dynamic Route Matching involves mapping routes with the same pattern to a single component.

 Routing - Vue Router - Dynamic Route Matching

src/
components/
BlastoiseCard.vue
CharizardCard.vue
EeveeCard.vue
GolbatCard.vue
MachampCard.vue
MoltresCard.vue
OnixCard.vue
VenusaurCard.vue
WigglytuffCard.vue
...Card.vue
AlakazamCard.vue
src/
components/
PokemonCard.vue

 Routing - Vue Router - Dynamic Route Matching

const routes = [
  {path: '/', component: CharizardCard},
   
   
   
  ...
];
  {path: '/pokemon/:id', component: PokemonCard}
  {path: '/charizard', component: CharizardCard},
  {path: '/blastoise', component: BlastoiseCard},
  {path: '/venusaur', component: VenusaurCard},

 Routing - Vue Router - Dynamic Route Matching

<template>
  ...
</template>

<script>
export default {
  name: "PokemonCard",
  created() {
    console.log(this.$route.params.id);
  }
};
</script>
PokemonCard.vue
/pokemon/charizard
charizard

 Routing - Vue Router - Dynamic Route Matching

data: [
  {
    id: 'charizard',
    hp: 78,
    type: 🔥,
    weight_lbs: 199,
    height_m: 1.7,
  },
  ...
]

 Routing - Vue Router - Dynamic Route Matching

export default [
  {
    id: "charizard",
    name: "Charizard",
    hp: 78,
    type: "🔥",
    weight_lbs: 199,
    height_m: 1.7
  },
  {
    id: "blastoise",
    name: "Blastoise",
    hp: 79,
    type: "💧",
    weight_lbs: 223,
    height_m: 1.6
  },
  {
    id: "venusaur",
    name: "Venusaur",
    hp: 80,
    type: "🍃",
    weight_lbs: 220,
    height_m: 2.0
  }
];
data.js

 Routing - Vue Router - Dynamic Route Matching

import Vue from "vue";
import VueRouter from "vue-router";
import PokemonCard from "./components/PokemonCard";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    component: {
      template: `
        <h3 class="subtitle has-text-white">
          Select a Pokémon from the links below!
        </h3>
      `
    }
  },
  {
    path: "/pokemon/:id",
    component: PokemonCard
  },
  {
    path: "*",
    component: {
      template: `
        <h3 class="subtitle has-text-white">
          Sorry. We couldn't find that Pokémon :(.
        </h3>
      `
    }
  }
];

export default new VueRouter({
  mode: "history",
  routes
});
router.js

 Routing - Vue Router - Dynamic Route Matching

<template>
  <div class="card has-text-weight-bold has-text-white"
    :class="'card--' + pokemon.id">
    <div class="card-image">
      <div class="card-image-container">
        <img :src="getPokemonImg"/>
      </div>
    </div>
    <div class="card-content has-text-centered">
      <div class="main">
        <div class="title has-text-white">{{pokemon.name}}</div>
        <div class="hp hp-venusaur">hp {{pokemon.hp}}</div>
      </div>
      <div class="stats columns is-mobile">
        <div class="column">{{pokemon.type}}<br>
          <span class="tag is-danger">Type</span>
        </div>
        <div class="column center-column">{{pokemon.weight_lbs}} lbs<br>
          <span class="tag is-danger">Weight</span>
        </div>
        <div class="column">{{pokemon.height_m}} m<br>
          <span class="tag is-danger">Height</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import pokemonData from '../data';

export default {
  name: "PokemonCard",
  data() {
    return {
      pokemon: {
        id: '',
        name: '',
        hp: 0,
        type: '',
        weight_lbs: 0,
        height_m: 0,
      },
    }
  },
  created() {
    this.getPokemon();
  }, 
  computed: {
    getPokemonImg() {
      // eslint-disable-next-line no-undef 
      return require(`../../static/${this.pokemon.id}.png`);
    }
  },
  watch: {
    $route() {
      this.getPokemon();
    }
  },
  methods: {
    getPokemon() {
      const idParam = this.$route.params.id;
      const pokemonFromData = pokemonData.find(pokemon => pokemon.id === idParam);

      if (pokemonFromData) {
        this.pokemon = pokemonFromData;
      }
    }
  }
};
</script>
PokemonCard.vue

 Routing - Vue Router - Dynamic Route Matching

Dynamic Route Matching with props is more ideal!

 Routing - Vue Router - Dynamic Route Matching

const routes = [
  ...,
  
  
  
  
  
  ...
];
  { 
    path: '/pokemon/:id',
    component: PokemonCard,
    props: true
  }

 Routing - Vue Router - Dynamic Route Matching

<template>
  ...
</template>

<script>
export default {
  name: "PokemonCard",
  props: ['id'],
  created() {
    console.log(this.id);
  }
};
</script>
PokemonCard.vue
/pokemon/charizard
charizard

How do we handle dynamic routes with unavailable params?

e.g.

https://localhost:8080/pokemon/pikachu

not available :(

<template>
  <div>
    <div v-if="pokemon.id">
      <!-- Display Pokemon Details -->
    </div>
    <div v-if="!pokemon.id">
      <h3 class="subtitle has-text-white">
        Sorry. We couldn't find that Pokémon :(.
      </h3>
    </div>
  </div>
</template>

<script>
  // ...
</script>
PokemonCard.vue

Nested Routes allows us to compose components multiple levels deep that corresponds to segments of a URL path.

 Routing - Vue Router - Nested Routes

 Routing - Vue Router - Nested Routes

App

PokemonCard

PokemonDetails

PokemonStats

 Routing - Vue Router - Nested Routes

/pokemon/:id/details
/pokemon/:id/stats
PokemonCard
PokemonDetails
PokemonCard
PokemonStats

 Routing - Vue Router - Nested Routes

const routes = [
  ...,
    







  
  
  
  

 

 

 

 


];
  { 
    path: '/pokemon/:id',
    component: PokemonCard,
    props: true,
    children: [
      {
        path: 'details',
        component: PokemonDetails,
        props: true
      },
      {
        path: 'stats',
        component: PokemonStats,
        props: true
      }
    ]
  }
router.js

 Routing - Vue Router - Nested Routes

<template>
  <div class="pokemon">
    <div class="card-image">
      ...
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
export default {
  name: "PokemonCard",
  ...
};
</script>
PokemonCard.vue
Responsible in rendering sub-component for nested URL route.

 Routing - Vue Router - Nested Routes

src/
components/
PokemonCard.vue
PokemonDetails.vue
PokemonStats.vue
App.vue
main.js

 Routing - Vue Router - Nested Routes

<template>
  <div class="card-content has-text-centered">
    <div class="main">
      <div class="title has-text-white">{{pokemon.name}}</div>
      <div class="hp hp-venusaur">hp {{pokemon.hp}}</div>
    </div>
  </div>
</template>

<script>
import pokemonData from '../data';

export default {
  name: "PokemonDetails",
  props: ['id'],
  data() {
    return {
      pokemon: {
        name: '',
        hp: 0
      },
    }
  },
  created() {
    this.getPokemon();
  }, 
  watch: {
    id() {
      this.getPokemon();
    }
  },
  methods: {
    getPokemon() {
      const idParam = this.id;
      const pokemonFromData = pokemonData.find(pokemon => pokemon.id === idParam);

      if (pokemonFromData) {
        this.pokemon.name = pokemonFromData.name;
        this.pokemon.hp = pokemonFromData.hp;
      }
    }
  }
};
</script>
PokemonDetails.vue

 Routing - Vue Router - Nested Routes

<template>
  <div class="card-content has-text-centered">
    <div class="stats columns is-mobile">
      <div class="column">{{pokemon.type}}<br>
        <span class="tag is-danger">Type</span>
      </div>
      <div class="column center-column">{{pokemon.weight_lbs}} lbs<br>
        <span class="tag is-danger">Weight</span>
      </div>
      <div class="column">{{pokemon.height_m}} m<br>
        <span class="tag is-danger">Height</span>
      </div>
    </div>
  </div>
</template>

<script>
import pokemonData from '../data';

export default {
  name: "PokemonStats",
  props: ['id'],
  data() {
    return {
      pokemon: {
        type: '',
        weight_lbs: 0,
        height_m: 0
      },
    }
  },
  created() {
    this.getPokemon();
  }, 
  watch: {
    id() {
      this.getPokemon();
    }
  },
  methods: {
    getPokemon() {
      const idParam = this.id;
      const pokemonFromData = pokemonData.find(pokemon => pokemon.id === idParam);

      if (pokemonFromData) {
        this.pokemon.type = pokemonFromData.type;
        this.pokemon.weight_lbs = pokemonFromData.weight_lbs;
        this.pokemon.height_m = pokemonFromData.height_m;
      }
    }
  }
};
</script>
PokemonStats.vue

 Routing - Vue Router - Nested Routes

import Vue from "vue";
import VueRouter from "vue-router";
import PokemonCard from "./components/PokemonCard";
import PokemonDetails from "./components/PokemonDetails";
import PokemonStats from "./components/PokemonStats";

Vue.use(VueRouter);

const routes = [
  ...,
  {
    path: "/pokemon/:id",
    component: PokemonCard,
    props: true,
    children: [
      {
        path: "details",
        component: PokemonDetails,
        props: true
      },
      {
        path: "stats",
        component: PokemonStats,
        props: true
      }
    ]
  },
  ...
];

export default new VueRouter({
  mode: "history",
  routes
});
router.js

 Routing - Vue Router - Nested Routes

<template>
  <div class="card has-text-weight-bold has-text-white"
    :class="'card--' + id">
    <div class="card-image">
      <div class="card-image-container">
        <img :src="getPokemonImg"/>
      </div>
    </div>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: "PokemonCard",
  props: ['id'],
  computed: {
    getPokemonImg() {
      // eslint-disable-next-line no-undef 
      return require(`../../static/${this.id}.png`);
    }
  }
};
</script>
PokemonCard.vue

Navigation Guards give us the ability to guard navigations by either redirecting it or cancelling it.

 Routing - Vue Router - Navigation Guards

 Routing - Vue Router - Navigation Guards

Three types of Navigation Guards

1. Global - for all navigation routes

2. Per-route - for a certain route

3. In-component - for certain components

 Routing - Vue Router - Navigation Guards

Global Navigation Guards

const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
  ...
});
to.path
next();
next('/about');
next(false);

 Routing - Vue Router - Navigation Guards

Global After Hooks

const router = new VueRouter({ ... });
router.afterEach((to, from) => {
  ...
});

After hooks are not navigation guards since the route is already complete at that moment!

 Routing - Vue Router - Navigation Guards

const routes = [
  ...,
    








];
  { 
    path: '/pokemon/:id',
    component: PokemonCard,
    props: true,
    beforeEnter: (to, from, next) => {
      ...
    }
  }

Per-Route Navigation Guards

 Routing - Vue Router - Navigation Guards

In-Component Navigation Guards

<template>
  ...
</template>

<script>
export default {
  name: "PokemonCard",
  beforeRouteEnter(to, from, next) {},
  beforeRouteUpdate(to, from, next) {},    beforeRouteLeave(to, from, next) {},
};
</script>

 Routing - Vue Router - Navigation Guards

import ...;

Vue.use(VueRouter);

const routes = [
  ...,
  {
    path: "*",
    component: {
      template: `
        <h3 class="subtitle has-text-white">
          Sorry. We couldn't find that Pokémon :(.
        </h3>
      `
    },
    beforeEnter: (to, from, next) => {
      console.log("Are you looking for a Pokemon? Use the links below!");
      next();
    }
  }
];

export default new VueRouter({
  mode: "history",
  routes
});
router.js

 Routing - Vue Router - Navigation Guards

<template>
  ...
</template>

<script>
import pokemonData from '../data';

export default {
  name: "PokemonCard",
  ...,
  beforeRouteUpdate(to, from, next) {
    if (to.path === '/pokemon/venusaur') next('/pokemon/charizard');
    else next();
  },
  ...
};
</script>
PokemonCard.vue

 Routing - Vue Router

Vue Router has more functionality:

 Routing - Vue Router

- Vue Router Documentation

- Let's build a Custom Vue Router

- Understanding client side routing with Vue.js: JSCamp18

 Routing - Chapter Summary!

  • Routing refers to splitting an applications UI based on rules derived from the browser URL.

  • Client-side applications are often labelled as Single-Page applications since the server provides a single template.

  • Vue Router is Vue's official client side routing library.

  • routes: Array that maps components to respective URL pathnames.

  • <router-view>: Component responsible in rendering another specified component based on the app's location.

  • <router-link>: Component that allows the user to change the location of the browser without making a web request.

  • Vue Router features we've seen

  • Dynamic Route Matching: mapping routes with the same pattern to a single component.

  • Nested Routes: compose components multiple levels deep that corresponds to segments of a URL path.

  • Navigation Guards: guard navigations by either redirecting it or cancelling it.

 Routing - Exercise 💪!

 Routing - Exercise 💪!

App

CityComponent

 Routing - Exercise 💪!

Starting Point

 Routing - Exercise 💪!

Vue Router

Creates routes to each different city with paths like ''/city/los-angeles".

Solution 👀

Create a router instance (e.g. router.js) that exports a router instance and contains a routes array.

Vue Router/Dynamic Routes

Creates a single dynamic route for the different cities: "/city/:id".

Solution 👀

Create a router instance (e.g. router.js) that exports a router instance and contains a routes array.

Create simple routes for homepage/not-found screens.

Create simple routes for homepage/not-found screens.

🎉

A Full Day of Vue.js Workshop - 05 Routing

By djirdehh

A Full Day of Vue.js Workshop - 05 Routing

  • 429