Skip to content

Instantly share code, notes, and snippets.

@looselytyped
Last active August 15, 2023 00:00
Show Gist options
  • Save looselytyped/3374c6803523164c373d768f316a1a49 to your computer and use it in GitHub Desktop.
Save looselytyped/3374c6803523164c373d768f316a1a49 to your computer and use it in GitHub Desktop.

FriendsHQ with Vue.js (3.x)

Links

Hello Vue

  • Getting started with create-vue
  • What do you have in the project you cloned?

Discussion

  • Creating new components, and constructing Component hierarchies
    • You have to import components where you'd like to use them
    • Vue introduces some magic so that @ is an alias to the src folder (See vite.config if curious)
    • You can use custom tags using PascalCase (HelloWorld) or kebab-case (<hello-world />)—Vue will resolve it correctly regardless
  • Before we get started—heads up—we'll be using the Composition API (as opposed to the traditional Options API) for today's workshop
    • More on this later

Exercise

  • Introduce a HelloWorld.vue component in src/components/HelloWorld.vue with this template
<template>Hello VueJs!</template>
  • Use that in App.vue by replacing the line that says <!-- When using HelloWorld use that component 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-row no-gutters class="flex-nowrap">
      <v-card class="d-flex justify-start mx-4" style="width: 100%">
        <v-list header style="width: 100%">
          <!-- Display the first friend's first and last name here -->
        </v-list>
      </v-card>
      <div class="d-flex justify-end mb-6">
        <v-btn color="success" dark large>Add Friend</v-btn>
      </div>
    </v-row>
  </v-container>
</template>
  • Use that in App.vue by replacing the line that says <!-- When using PeopleList replace this entire block -->

Discussion

  • To bind to the template, you need data
  • This data can be "static" (in other words, once rendered, an update to the data will not be reflected) in the template
  • OR it can be "reactive" (that is, if the data changes, the template reflects that change)
    • Vue offers two APIs to make data reactive, ref and reactive
<script setup>
import { ref } from "vue";

const count = ref(0);
console.log(count.value);
</script>
<template>
  <!-- Template uses the reactive count  -->
  <h1>{{ count }}</h1>
  <!-- Clicking the button increments the counter and the template automatically updates -->
  <button @click="count++" type="button">Add 1 to counter</button>
</template>

Exercise

  • Introduce the <script setup></script> block in PeopleList.vue. Note the use of setup in there.
  • Make the data of PeopleList a reactive array friends where each object looks like one from server/api/db.json
    • To make it reactive use the ref API offered from Vue
    • You'll have to import ref first using import { ref } from "vue";
  • 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
    • You can potentially supply v-for a v-bind:key so that Vue can optimize re-rendering—this isn't strictly necessary, but generally a good idea.
    • 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. -->
<v-list-item v-for="(friend, index) of friends" :key="friend.id">
  <v-list-item-title class="d-flex justify-start">
    {{ friend.firstName }}
    {{ friend.lastName }}
    <v-spacer />

    <div class="d-flex justify-end">
      <v-btn density="comfortable" variant="plain" icon>
        <v-icon class="edit"> mdi-pencil </v-icon>
      </v-btn>
      <v-btn
        density="comfortable"
        variant="plain"
        icon
      >
        <v-icon class="fav" color="red">
          mdi-heart
        </v-icon>
      </v-btn>
    </div>
  </v-list-item-title>

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

Discussion

  • We spoke about state management via reactive state.

  • "Method handlers" allow you operate on that "state"

    • The cool thing is that they are just regular JavaScript functions. Woot!
  • 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 "state" 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
  • In the composition API, you have a specific defineProps method that is automatically available to you. defineProps is supplied an array of all props the component needs.
  • 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
    • You want to grab all the code in v-list-item element (Be sure to strip out the v-for)
    • Declare the props for PersonItem using defineProps 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
    • Change the conditional in the v-divider to use last prop
    • 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) of friends" />
    • Do NOT forget to v-bind the props that your child component needs

Discussion

  • You can declare your props with some validation.
  • In this case, defineProps can take an object, in which every key is the name of the prop, and it's value is a nested object that defines it's type, whether is required or not, and a default value.

Exercise

  • Convert your props to use prop validation

Discussion

  • Vue offers you a way to "compute" (a.k.a derived) properties to avoid cluttering the template with complex logic
  • They are called computed properties for a reason!
  • Computed properties are different than methods b/c they only react to the properties they are "watching"
  • Just like ref (and reactive) you can import { computed } from "vue";
    • computed takes a function which should return the value of the property and returns the computed property

Exercise

  • Use a "computed" property in PersonItem.vue to calculate the fullName
    • Be sure import { computed } from "vue";

Discussion

You should not modify the props supplied to a component! It's not the components state—it's the parents. Vue warns you of this, and the resultant behavior can be unpredictable. Instead, you should "emit" events that notify the parent that it (the parent) should modify it's state.

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

Much like you defineProps, you can also defineEmits—both of these serve as the API of the component. props are the inputs the component needs, emits are the output.

The component can simply emit the event (See below), and it's parent can treat this like any other event using v-on like so

// child component, for example called ExampleComponent

const emit = defineEmits(["some-event"]);
emit('some-event', someValue);

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

Exercise

  • Make PersonItem a good citizen—it's currently modifying the friend prop in it's like method. Instead of modifying the friend prop, emit an event called friend-liked
    • Make sure PeopleList is listening for that event, and reacts appropriately

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. If you have not, this is the time to run npm run rest-endpoint in a second terminal.

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

Exercise

Before you do this, be sure you have npm run rest-endpoint 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 onMounted lifecycle hook.
      • You'll need to import { onMounted } from "vue";
    • axios.get returns you a resp object which has the payload in it's data property
    • To replace the value of a ref you need to set it's value property

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. For example, to update the fav property, you'd patch { 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:
<script setup>
import { ref } from "vue";
import { computed } from "vue";
import { onMounted } from "vue";

import axios from "axios";

const friends = ref([]);
const favCount = computed(() => friends.value.filter((f) => f.fav).length);

onMounted(async () => {
  const resp = await axios.get("http://localhost:3000/friends");
  friends.value = resp.data;
});
</script>

<template>
  <v-container fluid>
    <v-row>
      <v-col cols="2">
        <v-card>
          <v-card-item title="Contacts"></v-card-item>
          <v-card-text class="py-3">
            <v-row>
              <v-col class="text-h2">{{ friends.length }}</v-col>

              <v-col class="text-right">
                <v-icon color="error" icon="mdi-contacts" size="60"></v-icon>
              </v-col>
            </v-row>
          </v-card-text>
        </v-card>
      </v-col>
      <v-col cols="2">
        <v-card>
          <v-card-item title="Favs"></v-card-item>
          <v-card-text class="py-3">
            <v-row>
              <v-col class="text-h2">{{ favCount }}</v-col>

              <v-col class="text-right">
                <v-icon color="error" icon="mdi-heart" size="60"></v-icon>
              </v-col>
            </v-row>
          </v-card-text>
        </v-card>
      </v-col>
    </v-row>
  </v-container>
</template>

<style></style>
  • Introduce a new file called src/router/index.js with the following content:
import { createRouter, createWebHistory } from "vue-router";
import Dashboard from "@/components/Dashboard.vue";
import PeopleList from "@/components/PeopleList.vue";

const router = createRouter({
  history: createWebHistory(import.meta.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
    // },
  ],
});

export default router;
  • Introduce two paths—one to / that uses the Dashboard component, and one to /people that uses the PeopleList component

  • REPLACE main.js with the following

import { createApp } from "vue";

import App from "./App.vue";

// Vuetify
import "vuetify/styles";
import { createVuetify } from "vuetify";
import * as components from "vuetify/components";
import * as directives from "vuetify/directives";
import "@mdi/font/css/materialdesignicons.css"; // Ensure you are using css-loader

import router from "./router"; // import router

const vuetify = createVuetify({
  components,
  directives,
  icons: {
    defaultSet: "mdi",
  },
});

const app = createApp(App).use(vuetify).use(router); // have the app use the router
app.mount("#app");
  • Be sure to use <router-view /> in App.vue

  • Replace <v-list-item v-else :value="item"> in App.vue (Line 20) with <v-list-item v-else :value="item" :to="{ name: item.routeName }">

  • Replace the items array in App.vue with

 const items = [
  { icon: "mdi-view-dashboard", text: "Dashboard", routeName: "dashboard" },
  { icon: "mdi-contacts", text: "Contacts", routeName: "people" },
   { divider: true },
   { icon: "mdi-clock", text: "Stay tuned!" },
 ];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment