Skip to content

Instantly share code, notes, and snippets.

@looselytyped
Created June 12, 2019 13:27
Show Gist options
  • Save looselytyped/5826993f8ec64127aa9d7acc0ae0165b to your computer and use it in GitHub Desktop.
Save looselytyped/5826993f8ec64127aa9d7acc0ae0165b to your computer and use it in GitHub Desktop.
Web Apps with Vue.js

Web Apps with Vue

FriendsHq

NOTES

  • This requires a project created by the Vue CLI

  • I have this project — It is webpack based

  • Vue CLI works in tandem with package.json and NPM/Yarn, as well as the vue-cli-service to make its magic happen

  • The thing to bear in mind here is that we use SFCs

  • This is much like Angular where you write HTML/CSS/JS in separate blocks.

  • However it does complicate the tooling since you need your editor to be aware of the different blocks.

  • git checkout master

  • Clear out HelloWorld.vue, App.vue

  • In App.vue

<template>
  <div id="app">
    <h1>Hello!</h1>
  </div>
</template>
  • SHOW main.js — This is where the "mounting" happens.
  • And the index.html file
  • Show Elements tab in browser and show how app.js has been injected
  • Show Vetur extension in VS Code

Lets make a child component

  • In HelloWorld.vue
<template>
  <v-container>
    Hello VueJs!
  </v-container>
</template>

<script>
export default {};
</script>

<style></style>
  • In App.vue
<template>
  <HelloWorld />
</template>

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

export default {
  components: {
    HelloWorld
  }
};
</script>

EXERCISE

stash save --include-untracked
git checkout 4211172ea101d4bfbebfe541053f3bd07ed60bbd

Then CREATE ANOTHER CHILD COMPONENT AND USE IT IN APP

Couple of things to note

  • We do NOT see HelloWorld in the DOM. This is different than Angular

Lets flesh out our application - Replace all the code in App.vue

<template>
  <!-- Blatantly stolen from https://vuetifyjs.com/en/examples/layouts/sandbox -->
  <v-app id="sandbox">
    <v-navigation-drawer
      v-model="primaryDrawer.model"
      :permanent="primaryDrawer.type === 'permanent'"
      :temporary="primaryDrawer.type === 'temporary'"
      absolute
      overflow
      app
    >
      <div class="pa-3 text-xs-center teal white--text">
        <div class="display-2 py-4">
          Friends HQ
        </div>
        <p>Together, we are stronger</p>
      </div>
      <v-spacer></v-spacer>
      <v-list dense> </v-list>
    </v-navigation-drawer>
    <v-toolbar app absolute>
      <v-toolbar-side-icon
        v-if="primaryDrawer.type !== 'permanent'"
        @click.stop="primaryDrawer.model = !primaryDrawer.model"
      ></v-toolbar-side-icon>
    </v-toolbar>
    <v-content>
      <v-container fluid>
        <v-layout align-center justify-center>
          <v-flex xs10>
            <v-card>
              <v-card-text>
                <v-layout row wrap>
                  <HelloWorld />
                </v-layout>
              </v-card-text>
            </v-card>
          </v-flex>
        </v-layout>
      </v-container>
    </v-content>
    <v-footer :inset="true" app>
      <span class="px-3">
        &copy; Looselytyped {{ new Date().getFullYear() }}
      </span>
    </v-footer>
  </v-app>
</template>

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

export default {
  components: {
    HelloWorld
  },
  data: () => ({
    primaryDrawer: {
      model: null,
      type: "default (no property)"
    }
  })
};
</script>

Lets create a PeopleList

  • Delete HelloWorld.vue
  • Create PeopleList.vue
  • Update App.vue to use PeopleList.vue

PeopleList.vue

  • For the template
<template>
  <v-container fluid>
    <v-layout row>
      <v-flex grow pa-1>
        <v-card>
          <v-list header>
          </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>
  • Start with the friends array and data - in PeopleList.vue
<script>
const friends = [
  {
    id: 1,
    firstName: "Michelle",
    lastName: "Mulroy",
    gender: "female",
    fav: true
  },
  {
    id: 2,
    firstName: "Venkat",
    lastName: "Subramanian",
    gender: "male",
    fav: true
  },
  {
    id: 3,
    firstName: "Matt",
    lastName: "Forsythe",
    gender: "none",
    fav: false
  },
  {
    id: 4,
    firstName: "Nate",
    lastName: "Schutta",
    gender: "male",
    fav: false
  }
];

export default {
  data: () => {
    return {
      friends
    };
  }
};
</script>

In the template we can now do something like this

<v-list header>
  <ul>
    <li v-for="(friend, index) in friends" v-bind:key="friend.id">
      {{ friend.firstName }} {{ index }}
      <hr v-if="index === friends.length - 1" />
    </li>
  </ul>
</v-list>
  • We can replace v-bind:key="friend.id" with :key="friend.id"
  • We are also using v-if — there is also v-else and `v-else-if
  • There is also v-show ONLY toggles hidden property

Lets clean this component up - Replace the "template" in PeopleList.vue with

<template>
  <v-container fluid>
    <v-layout row>
      <v-flex grow pa-1>
        <v-card>
          <v-list header>
            <template v-for="(friend, index) in friends">
              <v-list-tile :key="friend.id" avatar ripple>
                <v-list-tile-content>
                  <v-list-tile-title>
                    {{ friend.firstName }} {{ friend.lastName }}
                  </v-list-tile-title>
                </v-list-tile-content>

                <v-list-tile-action>
                  <v-icon :color="'red'">favorite</v-icon>
                </v-list-tile-action>
              </v-list-tile>
              <!-- eslint-disable-next-line vue/valid-v-for -->
              <v-divider v-if="index + 1 < friends.length"></v-divider>
            </template>
          </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>
export default {
  data() {
    return {
      friends
    };
  }
};

Add a "click" method

Now we can listen for a click using v-on:click="like(friend)" — Make sure we have a like method on our Vue instance

We can also use @click in place of v-on

<template>
  <v-container fluid>
    <v-layout row>
      <v-flex grow pa-1>
        <v-card>
          <v-list header>
            <template v-for="(friend, index) in friends">
              <v-list-tile
                :key="friend.id"
                avatar
                ripple
                v-on:click="like(friend)" <-- ADD THIS LINE
              >
                <v-list-tile-content>
                  <v-list-tile-title
                    >{{ friend.firstName }}
                    {{ friend.lastName }}</v-list-tile-title
                  >
                </v-list-tile-content>

                <v-list-tile-action>
                  <v-icon :color="'red'">favorite</v-icon>
                </v-list-tile-action>
              </v-list-tile>
              <!-- eslint-disable-next-line vue/valid-v-for -->
              <v-divider v-if="index + 1 < friends.length"></v-divider>
            </template>
          </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>
export default {
  data() {
    return {
      friends
    };
  },
  methods: { <-- ADD methods block
    like(friend) {
      console.log(`${friend} changed`);
      friend.fav = !friend.fav;
    }
  }
};

Discussion

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

Conditionally change the class

NOTICE THAT WE ARE BINDING HERE

In PeopleList.vue change the v-icon to look like

<v-icon :color="friend.fav ? 'red' : 'grey'">favorite</v-icon>

Notice that we are binding here

Now lets make it so that we can simplify this component

  • Create PersonItem.vue file
<template>
  <div>
    <v-list-tile :key="friend.id" avatar ripple @click="like(friend)">
      <v-list-tile-content>
        <v-list-tile-title
          >{{ friend.firstName }} {{ friend.lastName }}</v-list-tile-title
        >
      </v-list-tile-content>

      <v-list-tile-action>
        <v-icon :color="friend.fav ? 'red' : 'grey'">favorite</v-icon>
      </v-list-tile-action>
    </v-list-tile>
    <!-- eslint-disable-next-line vue/valid-v-for -->
    <v-divider v-if="index + 1 < friends.length"></v-divider>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>
  • How do we get the friend and know its the last in here?
  • WE USE PROPS!

In PersonItem.vue file

export default {
  props: ["friend", "last"],
  methods: {
    like(friend) {
      friend.fav = !friend.fav;
    }
  }
};
  • Now change the template to use last
  • Now update PeopleList.vue to use that component
import PersonItem from "./PersonItem";

  components: {
    PersonItem
  },

NOTE THAT PROPS are just bindings! We can use v-bind or simply :

In People.vue

<PersonItem
  v-for="(friend, index) in friends"
  :key="friend.id"
  :friend="friend"
  :last="index + 1 < friends.length"
></PersonItem>

You can impose restrictions on props like so

In PersonItem.vue file change props to

props: {
  friend: {
    type: Object,
    required: true,
  },
  last: {
    type: Boolean,
    default: false
  }
},

Lets use a computed property to calculate the fullName

  • Computed properties ARE PROPERTIES !!! They are NOT METHODS
  • They are meant to GIVE A DIFFERENT VIEW of the data

In PersonItem.vue file

computed: {
  fullName() {
    return `${this.friend.firstName}, ${this.friend.lastName}`;
  }
}

THere are also filters but there isn't much you can do with filters that you can't do with computed properties

Now lets make our PersonItem dumb

  • How do children talk to parents? Via custom events!

  • In PersonItem.vue

methods: {
  notifyParent() {
    this.$emit("notify-parent", this.friend);
  }
}
    @click.stop="notifyParent"

NOTE THE MODIFIER THERE Show VUE -> Events in Browser

  • In PeopleList.vue
<PersonItem
  v-for="(friend, index) in friends"
  :key="friend.id"
  :friend="friend"
  :last="index + 1 < friends.length"
  @notify-parent="like"            <----- ADD THIS LINE
></PersonItem>

Idiomatically, event names are hyphen-case


EXERCISE

stash save --include-untracked
git checkout 9138a6fceb27787cef7cb74c5ed533941fb207aa

Then Add another prop to PersonItem with a default value and supply it from the parent

Swap out the hard-coded elements for axios

  • We will use one of the life-cycle methods, namely mounted

  • In PersonList.vue add

import axios from "axios";

mounted() {
    axios.get("http://localhost:3000/friends").then(response => {
        this.friends = response.data;
    });
}

You can use async/await here

  async mounted() {
    this.friends = (await axios.get("http://localhost:3000/friends")).data;
  },

We can use axios to PUT as well

  • In PersonList.vue update like method to do a PUT
axios.put(`http://localhost:3000/friends/${friend.id}`, friend);

The nice thing here is that we can even use window.http — no Angular modules and any such non-sense

Hash out the Dashboard component

  • Make Dashboard.vue
<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";

const friends = [];

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

<style></style>

Note here we have a computed property

Set up routing

  • We can install vue-router using vue add router

  • Introduce a router.js file next to main.js

  • NOTE here we are configuring the global Vue Object!

  • Vue Router is a plugin

import Vue from "vue";
import Router from "vue-router";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "Dashboard",
      component: Dashboard
    },
    {
      path: "/people",
      name: "People",
      component: PeopleList
    },
    {
      path: "*",
      redirect: "/"
    }
  ]
});

We are using Named routes here

  • Next we tell our app about our routes - edit main.js and supply router to our global Vue instance
import Vue from "vue";
import "./plugins/vuetify";
import App from "./App.vue";
import router from "./router";

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount("#app");

  • Finally we use it in App.vue ... use <router-view />
  • Link via router-link - In App.vue look for v-list dense in the template and add the following
<v-list dense>
  <router-link :to="{ name: 'Dashboard' }">Dashboard</router-link>
  <br />
  <router-link :to="{ name: 'People' }">People</router-link>
</v-list>

Clean App.vue up

<template>
  <!-- Blatantly stolen from https://vuetifyjs.com/en/examples/layouts/sandbox -->
  <v-app id="sandbox">
    <v-navigation-drawer
      v-model="primaryDrawer.model"
      :permanent="primaryDrawer.type === 'permanent'"
      :temporary="primaryDrawer.type === 'temporary'"
      absolute
      overflow
      app
    >
      <div class="pa-3 text-xs-center teal white--text">
        <div class="display-2 py-4">
          Friends HQ
        </div>
        <p>Together, we are stronger</p>
      </div>
      <v-spacer></v-spacer>
      <v-list dense>
        <template v-for="(item, i) in items">
          <v-divider dark v-if="item.divider" class="my-3" :key="i"></v-divider>
          <v-list-tile :key="i" v-else :to="{ name: item.routeName }">
            <v-list-tile-action>
              <v-icon>{{ item.icon }}</v-icon>
            </v-list-tile-action>
            <v-list-tile-content>
              <v-list-tile-title class="grey--text">
                {{ item.text }}
              </v-list-tile-title>
            </v-list-tile-content>
          </v-list-tile>
        </template>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar app absolute>
      <v-toolbar-side-icon
        v-if="primaryDrawer.type !== 'permanent'"
        @click.stop="primaryDrawer.model = !primaryDrawer.model"
      ></v-toolbar-side-icon>
    </v-toolbar>
    <v-content>
      <v-container fluid>
        <v-layout align-center justify-center>
          <v-flex xs10>
            <v-card>
              <v-card-text>
                <v-layout row wrap>
                  <router-view></router-view>
                </v-layout>
              </v-card-text>
            </v-card>
          </v-flex>
        </v-layout>
      </v-container>
    </v-content>
    <v-footer :inset="true" app>
      <span class="px-3">
        &copy; Looselytyped {{ new Date().getFullYear() }}
      </span>
    </v-footer>
  </v-app>
</template>

<script>
export default {
  data: () => ({
    primaryDrawer: {
      model: null,
      type: "default (no property)"
    },
    items: [
      { icon: "dashboard", text: "Dashboard", routeName: "Dashboard" },
      { icon: "contacts", text: "Contacts", routeName: "People" },
      { divider: true },
      { icon: "notes", text: "Journal" }
    ]
  })
};
</script>

Add the ability to add a friend

  • Modify routes.js to add a "child route"
{
  path: "/people",
  component: People,
  children: [
    {
      path: "",
      name: "People",
      component: PeopleList
    }
  ]
},
  • Then create views/People.vue
<template>
  <v-container fluid>
    <router-view />
  </v-container>
</template>
  • Fix PeopleList
<template>
  <v-container fluid>
    <v-layout row>
      <v-flex grow pa-1>
        <v-card>
          <v-list header>
            <PersonItem
              v-for="(friend, index) in friends"
              :key="friend.id"
              :friend="friend"
              :last="index + 1 < friends.length"
              @notify-parent="like"
            ></PersonItem>
          </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>

Forms

  • There are no "form objects" backing it — You gotta do your own validation and such.

  • In router.js

 {
   path: "/people",
   component: People,
   children: [
     {
       path: "",
       name: "People",
       component: PeopleList
     },
     {
       path: "add",
       name: "AddFriend",
       component: AddFriend
     }
   ]
 },

  • In AddFriend.vue
<template>
  <form @submit.prevent="submit">
    <p v-if="errors.length">
      <b>Please correct the following error(s):</b>
      <ul>
        <!-- eslint-disable-next-line vue/require-v-for-key -->
        <!-- <li v-for="error in errors">{{ error }}</li> -->
      </ul>
    </p>

    <div>
      <input type="text" v-model="firstName" />
    </div>
    <div>
      <input type="text" v-model="lastName" />
    </div>
    <button type="submit">Submit</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      firstName: "Raju",
      lastName: "Gandhi",
      errors: []
    };
  },
  methods: {
    submit() {
      this.errors = [];

      if (!this.firstName) {
        this.errors.push("First name is required");
      }
    }
  }
};
</script>

<style></style>
  • Update PeopleList.vue
<v-btn color="success" large :to="{ name: 'AddFriend' }">
  Add Friend
</v-btn>
  • To see the final form git checkout 1a3b04cd5129f434b1df069a6bb9bd7fe3fce872
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment