Categories
Vue

How to Listen for Props Changes in a Vue.js Component?

Listening for prop changes is something that we’ve to do often in Vue.js components.

In this article, we’ll look at how to listen for prop changes in our Vue.js components.

Watchers

Props are reactive, so we can watch for value changes with watchers.

To do this, we write:

App.vue

<template>
  <div id="app">
    <HelloWorld :name="name" />
    <button @click="name = name === 'james' ? 'jane' : 'james'">
      Change text
    </button>
  </div>
</template>

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

export default {
  name: "App",
  components: {
    HelloWorld,
  },
  data() {
    return {
      name: "james",
    };
  },
};
</script>

components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    name: String,
  },
  watch: {
    name(val) {
      console.log(val);
    },
  },
};
</script>

In App.vue , we have the name state which we pass into the HelloWorld component via the name prop.

Also, we have a button that changes the name state value when we click it.

Then in the HelloWorld component, we register the name prop with the props property.

And we watch the name prop with the watch property with name added inside it.

name is set to a method with the val parameter that lets us get the current value of the name prop.

Therefore, when we click Change text, we see the latest value of the name prop logged.

It should be consistent with the name value on the template.

We can also get the previous value of a prop with the second parameter of the watcher function.

For instance, we can write:

components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    name: String,
  },
  watch: {
    name(val, oldVal) {
      console.log(val, oldVal);
    },
  },
};
</script>

App.vue stays the same.

oldVal has the previous value of the name prop.

Therefore, we should see the current and previous value of the name prop logged when we click the Change text button.

We can also add the deep property to watch for deeply nested properties of an object prop.

And the immediately property set to true will watch initial value of the prop.

For instance, we can write:

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    name: String,
  },
  watch: {
    deep: true,
    immediate: true,
    name(val, oldVal) {
      console.log(val, oldVal);
    },
  },
};
</script>

We set them both to true so we can watch deeply nested properties and the initial value.

Conclusion

We can listen for prop changes within a Vue.js component with a watcher.

Categories
Vue

How to Get Query Parameters from a URL in Vue.js?

Getting query parameters from a URL is something that we’ve to do often in our Vue.js apps.

In this article, we’ll look at how to get query parameters from a URL in Vue.js.

Get Query Parameters from a URL

With Vue Router, we can get a query parameter in a route component’s URL easily.

For instance, we can write:

main.js

import Vue from "vue";
import App from "./App.vue";
import HelloWorld from "./views/HelloWorld.vue";
import VueRouter from "vue-router";

const routes = [{ path: "/hello", component: HelloWorld }];

Vue.config.productionTip = false;
Vue.use(VueRouter);

const router = new VueRouter({
  routes
});

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

App.vue

<template>
  <div id="app">
    <router-view></router-view>
  </div>
</template>

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

views/HelloWorld.vue

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      name: "",
    };
  },
  mounted() {
    this.name = this.$route.query.name;
  },
};
</script>

In main.js , we defined the routes that we can load with Vue Router in the routes array.

Then we call Vue.use(VueRouter) so that the dependencies like router-view and extra properties for components are added.

Next, we create the VueRouter instance with the routes .

And we pass the router into the Vue instance object to register the routes.

We get the name query parameter as we did in views/HelloWorld.vue .

this.$router.query.name has the value of the name query parameter.

Therefore, when we go to /#/hello?name=james, we see ‘hi, james’ displayed since we assigned it to this.name .

Another way to get the query string is to pass it in as props.

To do this, we write:

main.js

import Vue from "vue";
import App from "./App.vue";
import HelloWorld from "./views/HelloWorld.vue";
import VueRouter from "vue-router";

const routes = [
  {
    path: "/hello",
    component: HelloWorld,
    props: (route) => ({ name: route.query.name })
  }
];

Vue.config.productionTip = false;
Vue.use(VueRouter);

const router = new VueRouter({
  routes
});

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

Then we change views/HelloWorld.vue to:

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  props: {
    name: String,
  },
};
</script>

And App.vue stays the same.

The props property is set to a function that takes the route parameter.

And we return an object with the name property set to route.query.name .

In HelloWorld.vue , we register the name prop with the props property and display name directly in the template.

Therefore, when we go to /#/hello?name=james, we see ‘hi, james’ again.

URLSearchParams

If we don’t use Vue Router, then we can get the query parameter with the URLSearchParams constructor.

For instance, we can write:

main.js

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

Vue.config.productionTip = false;

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

App.vue

<template>
  <div id="app">
    <HelloWorld />
  </div>
</template>

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

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

components/HelloWorld.vue

<template>
  <div class="hello">
    <h1>hi, {{ name }}</h1>
  </div>
</template>

<script>
export default {
  name: "HelloWorld",
  data() {
    return {
      name: "",
    };
  },
  mounted() {
    const urlParams = new URLSearchParams(window.location.search);
    this.name = urlParams.get("name");
  },
};
</script>

We call the URLSearchParams constructor with window.location.search , which has the query string.

Then we can get the query parameter with the given key with urlParams.get .

We assigned it to this.name so we can use it in the template.

So if we go to /?name=james , we see ‘hi, james’ displayed.

Conclusion

We can get URL parameters within a component with or without Vue Router.

Categories
Vue

How to Properly Watch for Nested Data with Vue.js

Watching for deeply nested object states and props is something that we often have to do in a Vue.js app.

In this article, we’ll look at how to properly watch for nested data in Vue.js components.

Deep Watchers

We can add deep watchers to components within the watch property.

For instance, we can write:

App.vue

<template>
  <div id="app">
    <button
      @click="
        id++;
        getTodo(id);
      "
    >
      increment id
    </button>
    <Todo :todo="todo" />
  </div>
</template>

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

export default {
  name: "App",
  components: {
    Todo,
  },
  data() {
    return {
      todo: {},
      id: 1,
    };
  },
  methods: {
    async getTodo(id) {
      const results = await fetch(
        `https://jsonplaceholder.typicode.com/todos/${id}`
      );
      const data = await results.json();
      this.todo = data;
    },
  },
};
</script>

Todo.vue

<template>
  <div class="hello"></div>
</template>

<script>
export default {
  name: "Todo",
  props: {
    todo: Object,
  },
  watch: {
    todo: {
      handler(val) {
        console.log(val);
      },
      deep: true,
    },
  },
};
</script>

We have a button that calls getTodo when we get it to get a new todo object with the given id .

getTodo updates the todo reactive property from the response.

We pass the todo value to the todo prop of the Todo component.

Then in the Todo component, we watch the todo prop with the watch property.

We have the handler function with the val parameter to get the latest value.

deep is set to true so that we can watch for properties object props.

Computed Property

We can create a computed property to get the property from a reactive property.

For instance, we can write:

Todo.vue

<template>
  <div>{{ todoTitle }}</div>
</template>

<script>
export default {
  name: "Todo",
  props: {
    todo: Object,
  },
  computed: {
    todoTitle() {
      return this.todo.title;
    },
  },
};
</script>

We keep App.vue the same.

We created the todoTitle computed property that returns the this.todo.title property.

Then we can use it in the template like any other reactive property.

todoTitle will be updated whenever any property in the this.todo object is updated.

Watch a Property of an Object

We can also watch a property of an object.

For instance, we can write:

<template>
  <div class="hello"></div>
</template>

<script>
export default {
  name: "Todo",
  props: {
    todo: Object,
  },
  watch: {
    "todo.title": {
      handler(val) {
        console.log(val);
      },
    },
  },
};
</script>

Then the title property from the todo prop is watched with the watcher.

This is because 'todo.title' is the title property of the todo prop.

val would have the value of the title property of the todo prop.

We can get the newVal and oldVal from the watcher function:

<template>
  <div class="hello"></div>
</template>

<script>
export default {
  name: "Todo",
  props: {
    todo: Object,
  },
  watch: {
    "todo.title": {
      handler(newVal, oldVal) {
        console.log(newVal, oldVal);
      },
    },
  },
};
</script>

Conclusion

We can watch for nested properties with watchers by adding methods to the watch property object with a property path.

Also, we can set deep to true to watch for deeply nested properties.

Categories
Vue

Build a Drag and Drop App with Vue.js

Drag and drop is a feature of many interactive web apps. It provides an intuitive way for users to manipulate their data. Adding drag and drop feature is easy to add to Vue.js apps.

The App We Are Building

We will create a todo app that has 2 columns — a To Do column and a Done column. You can drag and drop between the two to change the status from to do to done and vice versa. To build the app, we use Vue Material library with the Vue Draggable package to make the app look good and provide the drag and drop ability easily. It will also have a navigation menu and a top bar.

Getting Started

To begin building the app, we start by installing Vue CLI. We install it by running npm i -g @vue/cli. After that, we can create the project. To do this, we run vue create todo-app. Instead of choosing the default, we customize the scaffolding of the app by choosing the alternative option and pick Vue Router, Babel, and CSS preprocessor. The scaffolding should be finished after following the instructions and then we are ready to add some libraries.

We need to add Axios for making HTTP requests, and the Vue Material library with the Vue Draggable packages we mentioned before to make our app prettier and to provide the drag and drop capabilities we desire respectively. In addition, we need the Vee Validate package to let us do form validation in our app. To install these packages, we run npm i axios vuedraggable vue-material vee-validate@2.2.14 .

Building the App

Now we can write some code. To start, we add a mixin to let us make our requests. To do this, we create a mixins folder and add a file called todoMixin.js, and we then put the following into our file:

const axios = require('axios');
const apiUrl = 'http://localhost:3000';

export const todoMixin = {
 methods: {
 getTodos() {
 return axios.get(`${apiUrl}/todos`);
 },

 addTodo(data) {
 return axios.post(`${apiUrl}/todos`, data);
 },

 editTodo(data) {
 return axios.put(`${apiUrl}/todos/${data.id}`, data);
 },

 deleteTodo(id) {
 return axios.delete(`${apiUrl}/todos/${id}`);
 }
 }
}

These functions will be used in our home page to make HTTP requests to do CRUD on our todo list. We will create the home page now. In Home.vue, we replace what we have with the following:

<template>
  <div class="home">
    <div class="center">
      <h1>To Do List</h1>
      <md-button class="md-raised" @click="showDialog = true"
        >Add Todo</md-button
      >
    </div>

    <div class="content">
      <md-dialog :md-active.sync="showDialog">
        <md-dialog-title>Add Todo</md-dialog-title>
        <form @submit="addNewTodo" novalidate>
          <md-field :class="{ 'md-invalid': errors.has('description') }">
            <label for="description">Description</label>
            <md-input
              type="text"
              name="description"
              v-model="taskData.description"
              v-validate="'required'"
            ></md-input>
            <span class="md-error" v-if="errors.has('description')">{{
              errors.first("description")
            }}</span>
          </md-field>

          <md-dialog-actions>
            <md-button class="md-primary" @click="showDialog = false"
              >Close</md-button
            >
            <md-button class="md-primary" @click="showDialog = false"
              >Save</md-button
            >
          </md-dialog-actions>
        </form>
      </md-dialog>
      <div class="lists">
        <div class="left">
          <h2>To Do</h2>
          <draggable v-model="todo" group="tasks" @change="updateTodo">
            <div v-for="t in todo" :key="t.id" class="item">
              {{ t.description }}
              <a @click="deleteTask(t.id)">
                <md-icon>close</md-icon>
              </a>
            </div>
          </draggable>
        </div>
        <div class="right">
          <h2>Done</h2>
          <draggable v-model="done" group="tasks" @change="updateTodo">
            <div v-for="d in done" :key="d.id" class="item">
              {{ d.description }}
              <a @click="deleteTask(d.id)">
                <md-icon>close</md-icon>
              </a>
            </div>
          </draggable>
        </div>
      </div>
    </div>
  </div>
</template>


<script>
// @ is an alias to /src
import draggable from "vuedraggable";
import { todoMixin } from "@/mixins/todoMixin";

export default {
  name: "home",
  components: {
    draggable,
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some((key) => this.fields[key].dirty);
    },
  },
  mixins: [todoMixin],
  data() {
    return {
      todo: [],
      done: [],
      showDialog: false,
      taskData: {},
    };
  },
  beforeMount() {
    this.getNewTodos();
  },
  methods: {
    async addNewTodo(evt) {
      evt.preventDefault();
      if (!this.isFormDirty || this.errors.items.length > 0) {
        return;
      }
      await this.addTodo(this.taskData);
      this.showDialog = false;
      this.getNewTodos();
    },

    async getNewTodos() {
      const response = await this.getTodos();
      this.todo = response.data.filter((t) => !t.done);
      this.done = response.data.filter((t) => t.done);
    },

    async updateTodo(evt) {
      let todo = evt.removed && evt.removed.element;
      if (todo) {
        todo.done = !todo.done;
        await this.editTodo(todo);
      }
    },

    async deleteTask(id) {
      const todo = await this.deleteTodo(id);
      this.getNewTodos();
    },
  },
};
</script>

<style lang="scss" scoped>
.center {
  text-align: center;
}

.md-dialog {
  width: 70vw;
}

form {
  width: 92%;
}

.md-dialog-title.md-title {
  color: black !important;
}

.lists {
  padding-left: 5vw;
  display: flex;
  align-items: flex-start;
  .left,
  .right {
    width: 45vw;
    padding: 20px;
    min-height: 200px;
    .item {
      padding: 10px;
      border: 1px solid black;
      background-color: white;
      display: flex;
      justify-content: space-between;
      a {
        cursor: pointer;
      }
    }
  }
}
</style>

We added a dialog box with a form to enter the description for our task. The field is required but the user can enter anything. The addTodo function takes the data entered and submit it if it’s valid. The this.fields function is provided by the Vee Validate package and has all the fields in the object so we can check if the fields have been changed or not. Anything in the computed property is computer whenever anything returned by the function changes.

Next, we added 2 lists that we drag between to let us change the status of the tasks to done or not done. The lists are the draggable components on the template. We defined the models in the draggable components in the object returned by the data function. It is important that we have the same group prop so that we can drag between them the 2 draggable components. Whenever drag and drop happens, the change event is raised and the updateTodo function is called. The function will toggle the done flag of the task and make a request to save the task.

Each task also has a button to delete it. When the close button is clicked, the deleteTodo function is called. The id of the task is passed into the function, so we can make the request to delete the task.

Next in App.vue, we add the menu and a left-side navigation bar with the following code:

<template>
  <div id="app">
    <md-toolbar>
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">Todo App</h3>
    </md-toolbar>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">Todo App</span>
      </md-toolbar>

      <md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Home</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>

    <router-view />
  </div>
</template>

<script>
export default {
  name: "app",
  data: () => {
    return {
      showNavigation: false,
    };
  },
};
</script>

<style>
.center {
  text-align: center;
}

form {
  width: 95vw;
  margin: 0 auto;
}

.md-toolbar.md-theme-default {
  background: #009688 !important;
  height: 60px;
}

.md-title,
.md-toolbar.md-theme-default .md-icon {
  color: #fff !important;
}
</style>

The <router-view /> displays the routes that we will define in router.js, which only consists of the home page.

In main.js,we put:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueMaterial from 'vue-material';
import VeeValidate from 'vee-validate';
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'

Vue.config.productionTip = false;

Vue.use(VueMaterial);
Vue.use(VeeValidate);

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

to include the Vue.js add-on libraries that we use in this app.

And in router.js, we add:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

This adds the home page to our list of routes so that it will be displayed to the user when typing in the URL or clicking a link to the page.

Our JSON API will be added without writing any code by using the JSON Server Node.js package, located at https://github.com/typicode/json-server. Data will be saved to a JSON file, so we don’t have to make our own back end add to save some simple data. We install the server by running npm i -g json-server. Then once that is done, go into our project directory then run json-server --watch db.json. In db.json, we put:

{
 "todos": []
}

so that we can use those endpoints for saving data to db.json, which have the same URLs as in todoMixin.

After all the work is done we have the following:

Categories
Vue 3 Projects

Create a Landing Page with Vue 3 and JavaScript

Vue 3 is the latest version of the easy to use Vue JavaScript framework that lets us create front end apps.

In this article, we’ll look at how to create a landing page with Vue 3 and JavaScript.

Create the Project

We can create the Vue project with Vue CLI.

To install it, we run:

npm install -g @vue/cli

with NPM or:

yarn global add @vue/cli

with Yarn.

Then we run:

vue create landing-page

and select all the default options to create the project.

Create the Landing Page

To create the landing page, we write:

<template>
  <form v-if="!submitted" @submit.prevent="submitted = true">
    <div>
      <label>your name</label>
      <input v-model.trim="name" />
    </div>
    <button type="submit">submit</button>
  </form>
  <div v-else>
    <p>Welcome {{ name }}</p>
    <p>The time is {{ dateTime }}</p>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      name: "",
      dateTime: new Date(),
      timer: undefined,
      submitted: false,
    };
  },
  methods: {
    setDate() {
      this.dateTime = new Date();
    },
  },
  mounted() {
    this.timer = setInterval(() => {
      this.setDate();
    }, 1000);
  },
  beforeUnmount() {
    clearInterval(this.timer);
  },
};
</script>

We have a form that lets the user submit their name.

Inside it, we have an input that binds to the name reactive property with v-model .

The trim modifier lets us trim leading and trailing whitespaces.

The form has the v-if directive to show the form if submitted is false .

And triggering the submit event with the submit button with set submitted to true .

Below that, we should the landing page content by showing the name and dateTime .

In the script tag, we have the data method to return our reactive properties.

The setDate method sets the dateTime to the current date.

The mounted hook calls setInterval to call setDate to update dateTime every second.

The beforeUnmount hook calls clearInterval to clear the timer when we unmount the component.

Conclusion

We can create a landing page with Vue 3 and JavaScript.