Skip to content

Instantly share code, notes, and snippets.

@looselytyped
Last active January 22, 2023 22:00
Show Gist options
  • Save looselytyped/b875bb104c364d996ed9107dd8604a21 to your computer and use it in GitHub Desktop.
Save looselytyped/b875bb104c364d996ed9107dd8604a21 to your computer and use it in GitHub Desktop.

FriendsHQ with Vue.js (2.x)

WARNING This gist is deprecated.

An updated one using Vue3 can be found here

Links

Hello Vue

  • Getting started with the Vue CLI
  • What do you have in the project you cloned?

(Almost) Everything you learn today, you can use with Vue 3!

Discussion

  • Creating new components, and constructing Component hierarchies

Exercise

  • Introduce a HelloWorld.vue component in src/components/HelloWorld.vue with this template
<template>
  <v-container>
    Hello VueJs!
  </v-container>
</template>
  • Use that in App.vue by replacing the line that says <!-- Insert your code here -->

Exercise

  • Refactor HelloWorld.vue to be PeopleList.vue (be sure to replace the custom element in App.vue)
  • Use this template
<template>
  <v-container fluid>
    <v-layout row>
      <v-flex grow pa-1>
        <v-card>
          <v-list header>
            <!-- Display the first friend's first and last name here -->
          </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>
  • Update App.vue so that you don't have nested v-container (there is one in App.vue and one in People.vue)

Discussion

  • data is the "state" of the component
  • To declare the "state" data needs to be a function
export default {
  // this is how you declare state
  data() {
    return {
      // typically you return a collection, usually an object
    };
  }
}

Exercise

  • Make the state of PeopleList an array friends where each object looks like one from server/api/db.json
  • Display the first friend's first and last name in the template

Discussion

  • To loop in Vue there is a v-for construct
  • There is another construct called v-bind that allows you to "bind" and attribute to an HTML element that is dynamically allocated
    • v-for needs v-bind:key so that Vue can optimize re-rendering
    • There is a shortcut for v-bind:key namely :key
  • v-for also provides a a mechanism to get the index of the element — you can use this to do interesting things
  • There is also a v-if allows us to conditionally render elements using a boolean

Exercise

  • Use the v-for to create on li element by looping over all the friends in the state and display the firstName and index of each friend
  • Use v-if to only display a hr element every BUT the last element

Exercise

  • Now let's pretty it up. Replace your hard work with the following
<!-- REPLACE ONLY THE UL and LI elements you wrote WITH THIS. -->

<template v-for="(friend, index) in friends">
  <v-list-item :key="friend.id">
    <v-list-item-content>
      <v-list-item-title>
        {{ friend.firstName }} {{ friend.lastName }}
      </v-list-item-title>
    </v-list-item-content>

    <v-btn icon>
      <v-icon class="edit">
        mdi-pencil
      </v-icon>
    </v-btn>
    <v-btn icon>
      <v-icon class="fav" color="grey">
        mdi-heart
      </v-icon>
    </v-btn>
  </v-list-item>

  <!-- eslint-disable-next-line vue/valid-v-for -->
  <v-divider v-if="index !== friends.length - 1"></v-divider>
</template>

Discussion

  • We spoke about state management via data.

  • methods allow you operate on that "state" — This is a new block that we are going to use next

  • To "get" an event, you use parentheses v-on:event syntax, and attach a handler to it

    • Like v-bind:attr has a shortcut (:attr), v-on:event has a shortcut, namely @event

Exercise

  • Attach a click handler so that when you click on the v-btn you invoke a handler called like that toggles the fav property of the friend you clicked on
  • Can you figure out how to bind the color on v-icon so that it switches between red or grey depending on friend.fav property

Discussion

  • With "data" and "methods" our Vue instance is a ViewModel — it has "instance" state, and "methods" operate on that data.

  • Next, can we simplify this component?

    • Do we need another component?
      • Whats the reuse potential?
      • Is there enough state to manage?
    • How do we loop over a list of items?
      • If we are looping over a child component, how do we supply it with what it needs?

Let's talk about refactoring components

  • Child elements may need "inputs" from parents. These are called props
  • props are just bindings, so you can v-bind the props from the parent component to pass those values in

Exercise

  • Create a PersonItem.vue file next to PeopleList.vue and extract all the code that displays a friend into it
    • Declare props so that PeopleList can pass in a friend and a conditional called last that tells you if this is the last friend — You can use this to decide whether or not to display the v-divider
    • Be sure to have a like method in PersonItem.vue since your @click handler expects it
  • Be sure to "import" PersonItem in PeopleList and use that with a v-for like so <PersonItem v-for="(friend, index) in friends" />
    • Do NOT forget to v-bind the props that your child component needs

Discussion

  • You can declare your props as typed objects

Exercise

  • Convert your props to typed objects

Discussion

  • Vue offers you a way to "compute" (a.k.a derived) properties
  • Computed properties are different than methods b/c they only react to the properties they are "watching"

Exercise

  • Use a "computed" property in PersonItem.vue to calculate the "fullName"

Discussion

Smart vs. dumb components

  • Smart components
    • Usually leverage services or know about endpoints
    • Know how to get/load/update data
    • Dumb components
      • Fully defined via their API
      • Everything is a prop or an event

If props are inputs to components, then you can "emit" events, which act as "outputs"

The API for the same is $emit on the Vue instance itself. Parents can treat this like any other event using v-on like so

// child component, for example called ExampleComponent
this.$emit('some-event', someValue);

// parent component
<example-component @some-event="someHandler()">

Exercise

  • Make PersonItem dumb. Instead of modifying the friend prop, emit it, and update it in the parent component

Discussion

Performing asynchronous operations, like communicating with the backend is usually done using standard browser APIs (like fetch) or third-party libraries (like axios). Vue does not have any in-built support for this.

In our case, there is an endpoint at http://localhost:3000/friends — You can fetch all your friends from there.

We need a mechanism that allows us to intercept the lifecycle of the component. Vue gives us a few methods, one of which is mounted. We can use this as a place to put our Ajax calls.

Exercise

Before you do this, be sure you have yarn run server or npm run server in a terminal.

  • Replace the hard-coded array of friends in PeopleList with a call using axios (this is already in your package.json). The end-point is http://localhost:3000/friends. Be sure to introduce the mounted lifecycle hook.

Exercise

  • See if you can figure out how to update to a friend using the patch method in axios in the PeopleList#like — The endpoint you are looking for is http://localhost:3000/friends but you have to pass the friend.id as part of the URL (like http://localhost:3000/friends/1) and you have to send the updated property. Here is a snippet you can use
await axios.patch(`http://localhost:3000/friends/${friend.id}`, {
  fav: friend.fav
});

Discussion — Routing

  • Routing gives you a mechanism to replaces pieces of the DOM based on the application URL
  • Routing is provided by a plugin, namely vue-router — This has some configuration you need to apply with it
    • The primary piece of configuration that affects us is to define which "path" uses which component

Exercise — Routing

Introduce the following files

  • Create a new Dashboard component in the components folder with the following content:
<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";


export default {
  data() {
    return {
      friends: []
    };
  },
  computed: {
    favCount() {
      return this.friends.filter(f => f.fav).length;
    }
  },
  async mounted() {
    const resp = await axios.get("http://localhost:3000/friends");
    this.friends = resp.data;
  }
};
</script>

<style></style>
  • Introduce a new file called src/router.js with the following content:
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    // introduce routes here for the following — DO NOT FORGET TO NAME THEM!
    // "/" uses "Dashboard"
    // "/people" uses "PeopleList"
    // all other routes redirects to "/"

    // Here is an example
    // {
    //   path: "/",
    //   name: "Dashboard",
    //   component: Dashboard
    // },
  ]
});
  • REPLACE main.js with the following
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import router from "./router";

Vue.config.productionTip = false;

new Vue({
  vuetify,
  router,
  render: h => h(App)
}).$mount("#app");
  • Be sure to use <router-view /> in App.vue
  • Replace <v-list-item :key="i" v-else :id="item.text.toLowerCase()"> in App.vue (Line 20) with
<v-list-item
  :key="i"
  v-else
  :to="{ name: item.routeName }"
  :id="item.text.toLowerCase()"
>
  • Replace the items array in App.vue with
items: [
  { icon: "mdi-view-dashboard", text: "Dashboard", routeName: "Dashboard" },
  { icon: "mdi-contacts", text: "Contacts", routeName: "People" },
  { divider: true },
  { icon: "mdi-message-bulleted", text: "Journal" }
]

Discussion — Child routing

  • How do you go about adding/editing friends? Ideally we want /people/add and /people/:id/edit
    • Which means we need nested routes under /people
    • But where do we put the router-view element?

Exercise — Child routing

  • Create a new file views/People.vue that looks like this
<template>
  <v-container fluid>
    <router-view />
  </v-container>
</template>
  • Modify router.js to have a child route, like so:
{
  path: "/people",
  component: People,
  children: [
    {
      path: "",
      name: "People",
      component: PeopleList
    }
  ]
},
  • Remove the v-container tags from components/PeopleList.vue template

Discussion — Adding friends

  • To add we need a new child route /people/add and a corresponding component

Exercise — Adding friends

  • Create a new component components/AddEditFriend.vue with some boilerplate tempalte
  • Add the child route /people/add that uses AddEditFriend as the component with the name AddEditFriend
  • Modify PeopleList.vue so clicking the v-btn routes to the the AddEditFriend route, like so:
<v-btn color="success" large :to="{ name: 'AddEditFriend' }">
  Add Friend
</v-btn>

Discussion — Adding friends

  • Vue has no "forms" support. You get two-way binding with v-model though
  • You might reach into a 3rd party library to make managing forms easier

Exercise — Finish up AddEditFriend.vue

<template>
  <v-form ref="form" v-model="valid">
    <v-text-field
      v-model="selectedFriend.firstName"
      :rules="nameRules"
      label="First name"
      id="firstName"
      required
    ></v-text-field>

    <v-text-field
      v-model="selectedFriend.lastName"
      :rules="nameRules"
      label="Last name"
      id="lastName"
      required
    ></v-text-field>

    <v-checkbox
      v-model="selectedFriend.fav"
      label="Fav?"
      id="fav"
      required
    ></v-checkbox>

    <v-radio-group v-model="selectedFriend.gender" id="gender" row>
      <v-radio label="Male" :value="genders.male"></v-radio>
      <v-radio label="Female" :value="genders.female"></v-radio>
      <v-radio label="Undisclosed" :value="genders.undisclosed"></v-radio>
    </v-radio-group>

    <v-btn class="ma-2" color="error" @click="reset">
      Reset Form
    </v-btn>

    <v-btn
      class="ma-2"
      color="success"
      :disabled="!valid"pvue
      id="submit"
      @click="submit"
    >
      Submit
    </v-btn>

    <v-btn class="ma-2" :to="{ name: 'People' }">Cancel</v-btn>
  </v-form>
</template>

<script>
import axios from "axios";

export default {
  data() {
    return {
      valid: true,
      selectedFriend: {
        firstName: "",
        lastName: "",
        gender: "male",
        fav: false
      },
      genders: {
        male: "male",
        female: "female",
        undisclosed: "undisclosed"
      },
      nameRules: [v => !!v || "Name is required"]
    };
  },
  methods: {
    reset() {
      this.$refs.form.reset();
    },
    async submit() {
      const submission = {
        ...this.selectedFriend
      };
      await axios.post("http://localhost:3000/friends", submission);
      this.$router.push({ name: "People" });
    }
  }
};
</script>

<style></style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment