Vue Introduction

From bibbleWiki
Jump to navigation Jump to search

Intro

Hello John (No world)

Resource for the tutorial was https://github.com/johnpapa/vue-getting-started
The vue js lives at https://cdn.jsdelivr.net/npm/vue. Below is a simple example just showing two way binding

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
    <div id="app">
        <input type="text" v-model="name">
        <p>Hello {{name}}</p>
    </div>
    <script>
        new Vue({
            el: "#app",
            data() {
                return {
                    name: "John"
                }
            }
        })
    </script>
</body>
</html>

Sections

Vue files are comprised of three sections. No prizes for what each bit does

<template>
<a v-bind:href="github" target="_blank">
</a>
<template/>

<script>
</script>

<style>
</style>

Displaying Data and Events

Data Model

We can define a data model and use it with standard double curly braces.

</template>
...
<div>{{hero.id}}</div>
...
</template>

<script>
export default {
  name: 'Heroes',
  data() {
    return {
      id: 20,
      firstName: 'Fred',
      lastName: 'Bloggs'
      message: ''
    }
  },
};
</script>


Data Binding

Binding we can use v-bind or the short cut : (full colon). So in the template put the colon and define a data component below

<template>
...
          <a
            v-bind:="github"
            target="_blank"
            rel="noopener noreferrer"
          >
            <i class="fab fa-github fa-2x" aria-hidden="true"></i>
          </a>

          <a
            :href="twitter"
            target="_blank"
            rel="noopener noreferrer"
          >
            <i class="fab fa-twitter fa-2x" aria-hidden="true"></i>
          </a>
...
</template>

<script>
export default {
  name: 'headerLinks',
  data() {
    return {
      github: 'https://github.com/johnpapa/vue-getting-started',
      twitter:'https://twitter.com/john_papa',
    }
  },
}
</script>

Two Way Binding

To bind two-way with v-model with the model and property.

</template>
   <input class="input" id="firstName" v-model="hero.firstName"/>
</template>

Notation

Event Binding

Example

To bind events we use v-on: or @ to bind our methods to an event.

</template>
<button @click="cancelHero">Cancel</button>
<button v-on:click="cancelHero">Cancel 2</button>
</template>
<script>
export default {
  name: 'Heroes',
  data() {
...
  },
  methods: {
    cancelHero() {
       this.message = ''
    },
  }
};
</script>

Keyup

Below is an example of binding to the keyup event. The .esc denotes the escape key. </template> <script>

               <select
                 id="power"
                 v-model="hero.power"
                 :class="{ invalid: !hero.power }"
                  @keyup.esc="clearPower"
               >

</script> </syntaxhighlight>

Checkbox, Radio

No surprises here

<template>
                  <input
                    type="radio"
                    id="color-red"
                    value="red"
                    v-model="hero.capeColor"
                  />

                  <input
                    type="checkbox"
                    class="is-primary"
                    id="active"
                    v-model="hero.active"
                  />
</template>

Styles and Classes

Styles and Classes are trickier because of the object approach of the data

<template>
                <select
                  id="power"
                  v-model="hero.power"
                   @keyup.esc="clearPower"
                >

                <div
                  class="color-line"
                  :style="{ 'background-color': hero.capeColor }"
                ></div>
</template>

Displaying List and Conditional Content

Iterating v-for

So just create a key and use the right syntax and the defined model

<template>
        <ul class="list is-hoverable">
          <li v-for="hero in heroes" :key="hero.id">
            <a class="list-item"><span>{{ hero.firstName }}</span></a>
          </li>
        </ul>
</template>

Binding to selection

We can do this by binding to the model on click. Note the conditional class based on if the current hero === selected hero.

<template>
          <li v-for="hero in heroes" :key="hero.id">
            <a
              class="list-item"
              @click="selectedHero = hero"
              :class="{ 'is-active': selectedHero === hero }"
              ><span>{{ hero.firstName }}</span></a
            >
          </li>

</template>

Conditional Displaying v-if and v-show

v-if

Same as *ngIf. So if no selection on the list

<template>
    <div class="columns" v-if="selectedHero">
      <div class="column is-3">
        <header class="card-header">
          <p class="card-header-title">{{ selectedHero.firstName }}</p>
        </header>
        <div class="card-content">
          <div class="content">
...
</template>

v-show

This will put the data in dom.

<template>
            <div class="field" v-show="showMore">
              <label class="label" for="lastName">last name</label>
              <input
                class="input"
                id="lastName"
                v-model="selectedHero.lastName"
              />
            </div>
</template>

Interacting within a Component

Computed

This is a section in the scripts section which allow you to define function to compute value maybe from existing model data

<script>
  computed: {
    fullName() {
      return `${this.selectedHero.firstName} ${this.selectedHero.lastName}`;
    },
  },

</script>

Component Lifecycle Hooks

Here are the component lifecycle hooks for Vue. Component lifecyle vue.png So here is an example of the created below. Note the lifecycle hooks do not go in the methods section but are at the same level as methods: and computed:

<script>
...
  created() {
    this.loadHeroes();
  },
  methods: {
    async getHeroes() {
      return new Promise(resolve => {
        setTimeout(() => resolve(ourHeroes), 1500);
      });
    },
    async loadHeroes() {
      this.heroes = [];
      this.message = 'getting the heroes, please be patient';
      this.heroes = await this.getHeroes();
      this.message = '';
    },
...
</script>

Watchers

These are function which watch properties. When a properties changes you can perform some code. The immediate means it will run on startup up.

<script>
  watch: {
    'selectedHero.capeCounter': {
      immediate: true,
      handler(newValue, oldValue) {
        console.log(`Watcher evalauted. old=${oldValue}, new=${newValue}`);
        this.handleTheCapes(newValue);
      },
    },
  },
</script>

Filters (Example of date-fns too)

Filters are pipes you can use in Vue. Below is a recap of the structure along with the example of a filter (pipe). Note you can define custom pipes.

<template>
...
                <p class="comment">
                  My origin story began on
                  {{ selectedHero.originDate | shortDate }}
                </p>
...
</template>
<script>
import { format } from 'date-fns';
const inputDateFormat = 'YYYY-MM-DD';
const displayDateFormat = 'MMM DD, YYYY';
const ourHeros = [
... // Pseudo Data
]
export default {
   name: 'Heros',
   data() {
   ... // View Data Model
   }
   computed() {
   ... // Computed values
   }
   created() {
   ... // Life Cycle Hook
   }
   methods() {
   ... // Methods
   }
   watch() {
   ... // Watchers
   }
   filters() {
      shortDate: function(value) {
         return format(value, displayDateFormat);
      },   
   }
}
<script>

Component Communication

Using sub components

To do this you simple need to import the code, state it as a component in the parent and reference the name

<template>
...
<HeroDetail />
...
</template>

<script>
...
import HeroDetail from '@/components/hero-detail';
...
  components: {
    HeroDetail,
  },
...
</script>

Passing Props

Naming

This is horrible. For passing parameters you must use kebab-case and not camelCase. Argghhhhhhhhhhhhhhhh

<child-component :authorized-user="AuthorizedUser"></child-component>

Types

String, Number, Boolean, Array, Object, Function, Promise

Dynamic and Static

// Dynamic
:title="hero.name"

// Static
title="Mrs Jones"

Defining Child Component Props

In the child component we can define out @Input properties. We can also define defaults and validation on these properties too.

<script>
  props: {
    hero: {
      type: Object,
      default: () => {},
      validator: function (value) {
        // The value must match one of these strings
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      },
    },
  },
</script>

Child Component Updates

This is much the same as Angular where we emit the changes and pass the new value to the parent. YOU DO NOT MUTATE IN THE CHILD. Vue emit.png The odd thing is you do not list the parameters on the parent in the template but you do in the method.

Mixins

These are functions you can import into your component. Sound like a nightmare to me but hey.

  • Methods, Components and Computes
    • Merged, precedence given to the components version
  • Data
    • Merged superset, precedence given to the components data
  • Watch and Hooks
    • Both run, with mixins running before components

Example shown below but not convinced yet.

<script>
import { displayDateFormat, lifecycleHooks } from '../shared';
...
mixins: [lifecycleHooks],
...
</script>


And the mixin code

export const lifecycleHooks = {
  // Computeds
  computed: {
    componentName() {
      return `${this.$options.name} component`;
    },
  },
  // LifeCycle Hooks
  created() {
    logger.info(`${this.componentName} created ${hookMessageSuffix}`);
    logger.info('component data', this.$data);
  },
  mounted() {
    logger.info(`${this.componentName} mounted ${hookMessageSuffix}`);
  },
  updated() {
    logger.info(`${this.componentName} updated ${hookMessageSuffix}`);
  },
  destroyed() {
    logger.info(`${this.componentName} destroyed ${hookMessageSuffix}`);
  },
};

Data Access

Axios

This is used within the demos. So we see standard js.

const response = await axios({
  method: 'post',
  url: '/api/heros/717',
  header: {
   'X-Custom-Header': 'foo',
  },
  data: {
     firstName: 'Ella',
     lastName : 'Papa',
  },
})

Config

Again a bit of a disappointment. Nothing other than create a config.js

export const API = process.env.VUE_APP_API;
...

Mapping and Errors

No special approach in Vue, just use map and use try/catch

const getHeroes = async function() {
  // cant just return this, because its not what we want
  // return response.data;
  // but what if there is bad data in the response?
  // let data = response.data;
  // Let's parse it better
  try {
    const response = await axios.get(`${API}/heroes.json`);

    let data = parseList(response);

    const heroes = data.map(h => {
      h.originDate = format(h.originDate, inputDateFormat);
      return h;
    });
    return heroes;
  } catch (error) {
    console.error(error);
    return [];
  }
};

const parseList = response => {
  if (response.status !== 200) throw Error(response.message);
  if (!response.data) return [];
  let list = response.data;
  if (typeof list !== 'object') {
    list = [];
  }
  return list;
};

Routing

Install the Vue Router

vue add router

Define Routes

No much difference between this and Angular. The history defines if we should use the #location syntax.

import Vue from 'vue';
import Router from 'vue-router';
import PageNotFound from '@/views/page-not-found';

Vue.use(Router);

const parseProps = r => ({ id: parseInt(r.params.id) });

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      redirect: '/heroes',
    },
    {
      path: '/heroes',
      name: 'heroes',
      component: () =>
        import(/* webpackChunkName: "bundle.heroes" */ './views/heroes.vue'),
    },
    {
      path: '/heroes/:id',
      name: 'hero-detail',
      // props: true,
      props: parseProps,
      component: () =>
        import(/* webpackChunkName: "bundle.heroes" */ './views/hero-detail.vue'),
    },
    {
      path: '/about',
      name: 'about',
      component: () =>
        import(/* webpackChunkName: "bundle.about" */ './views/about.vue'),
    },
    {
      path: '*',
      component: PageNotFound,
    },
  ],
});

Navigation

Here is the navbar

<template>
  <nav class="column is-2 menu">
    <p class="menu-label">Menu</p>
    <ul class="menu-list">
      <router-link to="/heroes">Heroes</router-link>
      <router-link to="/about">About</router-link>
    </ul>
  </nav>
</template>

Parameters on Routes

Original Code

In the original code we had

                  <button
                    class="link card-footer-item"
                    @click="selectHero(hero)"
                  >
                    <i class="fas fa-check"></i>
                    <span>Select</span>
                  </button>

Implemented Router Link

The link is implemented using params to pass the id to the detail.

                  <router-link
                    tag="button"
                    class="link card-footer-item"
                    :to="{ name: 'hero-detail', params: { id: hero.id } }"
                  >
                    <i class="fas fa-check"></i>
                    <span>Select</span>
                  </router-link>

Passing The Parameters in the Router

We can manipulate the parameters in the router to make sure the types match. In the above example the id is a string and the props in the component expects a string.

const parseProps = (r) => ({ id: parseInt(r.params.id) });
...
    {
      path: '/heroes/:id',
      name: 'hero-detail',
      // props: true,
      props: parseProps,
      component: () =>
        import(
          /* webpackChunkName: "bundle.heroes" */ './views/hero-detail.vue'
        ),
    },
...

Define Output

This is generally in the app.vue

<template>
  <div id="app">
    <HeaderBar />
    <div class="main-section columns">
      <NavBar />
      <main class="column">
        <router-view />
      </main>
    </div>
  </div>
</template>

Lazy Load

For lazy loading to work we use the component syntax. The webpackChunkName is managed by webpack (err)

    {
      path: '/heroes',
      name: 'heroes',
      // No Lazy
      // component: HeroDetail, 

      // Lazy
      component: () =>
        import(/* webpackChunkName: "bundle.heroes" */ './views/heroes.vue'),
    },

Routing on Exit

When we leave page on save or cancel we can achieve this by

  methods: {
    cancelHero() {
      this.$router.push({ name: 'heroes' });
    },
    async saveHero() {
      await dataService.updateHero(this.hero);
      this.$router.push({ name: 'heroes' });
    },
  },

Managing State With Vuex

Introduction

Look very similar to redux. Vuex.png

Implementing

  • Add Vuex
  • Define Store
  • Communicate between components and store

Add Vuex

Easy pezzy, adds a store.js

vue add vuex

Define Store

Start with the following

import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const state = () => ({
});

const mutations = {
};

const actions = {
};

const getters = {
};

export default new Vuex.Store({
  strict: process.env.NODE_ENV !== 'production',
  state,
  mutations,
  actions,
  getters,
});

Strict Mode

In the Store we define

strict: process.env.NODE_ENV !== 'production',

This checks that we are not modifying the state without a mutation and should only be used in development.

State

We can add what we want to store in the state. In our example it is an array of heroes so

const state = () => ({
   heroes[],
});

Mutations Actions

Version 1

Need to describe how it works and then modify it a bit. We create an action and a mutations which are tightly coupled. The mutation name must match then name passed into the commit. Still very reduxy.

const mutations = {
  AddHero](state, hero) {
    state.heroes.push(hero); // mutable addition
  },
...
}
const actions = {
  async addHeroAction({ commit }, hero) {
    const addedHero = await dataService.addHero(hero);
    commit('AddHero', addedHero);
...
  },
}

Version 2

They put the names in a file and capitalize. It just looks ugly but makes sense.

export const ADD_HERO = 'addHero';

const mutations = {
  [ADD_HERO](state, hero) {
    state.heroes.push(hero); // mutable addition
  },
...
}
const actions = {
  async addHeroAction({ commit }, hero) {
    const addedHero = await dataService.addHero(hero);
    commit(ADD_HERO, addedHero);
...
  },
}

Set of Mutations for CRUD

Below is example CRUD mutations for a Store

const mutations = {
  [ADD_HERO](state, hero) {
    state.heroes.push(hero); // mutable addition
  },
  [UPDATE_HERO](state, hero) {
    const index = state.heroes.findIndex(h => h.id === hero.id);
    state.heroes.splice(index, 1, hero);
    state.heroes = [...state.heroes];
  },
  [GET_HEROES](state, heroes) {
    state.heroes = heroes;
  },
  [DELETE_HERO](state, heroId) {
    state.heroes = [...state.heroes.filter(p => p.id !== heroId)];
  },
};

Set of Actions for CRUD

We need to make sure the dataService is kept in sync.

const actions = {
  // actions let us get to ({ state, getters, commit, dispatch }) {
  async addHeroAction({ commit }, hero) {
    const addedHero = await dataService.addHero(hero);
    commit(ADD_HERO, addedHero);
  },
  async deleteHeroAction({ commit }, hero) {
    const deletedHeroId = await dataService.deleteHero(hero);
    commit(DELETE_HERO, deletedHeroId);
  },
  async getHeroesAction({ commit }) {
    const heroes = await dataService.getHeroes();
    commit(GET_HEROES, heroes);
  },
  async updateHeroAction({ commit }, hero) {
    const updatedHero = await dataService.updateHero(hero);
    commit(UPDATE_HERO, updatedHero);
  },
};

Getters

These allow you to define how to get part of the state. E.g. a single hero

const getters = {
  // heroes: state => state.heroes,
  // parameterized getters are not cached. so this is just a convenience to get the state.
  getHeroById: state => id => state.heroes.find(h => h.id === id),
};

Tell app about store

Communicate between components and store

State

In the components we can now use the state. As the source of truth exists we can now use the store to get this information using the vuex api in the computed section

computed: {
   ...mapState({'heros': 'heros'})
// or if they match names
   ...mapState(['heros'])
}

Loading State

In the loadHeroes function called in created() we can use the action created in the store.

await this.getHeroesAction();

Editing A Hero

In details page we need to get the hero from state. We do this we need to map the getters from the store and use it to get the hero. Note the use of lodash cloneDeep as we do not want to modify the state directly

await this.getHeroesAction();
  created() {
    if (this.isAddMode) {
...
    } else {
      // this.hero = await dataService.getHero(this.id);
      // this.hero = { ...this.getHeroById(this.id) };
      this.hero = cloneDeep(this.getHeroById(this.id));
    }
  },
...
computed: {
    // 1. mapping Getters
    // ...mapGetters({ getHeroById: 'getHeroById' }),
    // 2. shortcut for mapping Getters
    ...mapGetters(['getHeroById']),
...
}