Build a real-time video chat app with Vue and Daily Prebuilt in under ten minutes
Tags: vue, javascript, tutorial, webdev
Published: 2021-08-30 17:54:37

---
title: Build a real-time video chat app with Vue and Daily Prebuilt in under ten minutes
published: true
description: Embed Daily Prebuilt and add custom video call controls to any Vue app
tags: vue, javascript, tutorial, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q0qgpr62qpvdyoqal68s.png
canonical_url: https://www.daily.co/blog/build-a-video-chat-app-with-vue-and-daily-prebuilt/
---

At Daily, we’ve spent a lot of time making sure our video and audio-only APIs can be used with any frontend framework, or no framework at all. 🍦 It’s important to us to make flexible APIs that can be incorporated into any app looking to add audio and video chat. We’ve created several demos for our customers using [plain JavaScript](https://github.com/daily-demos/prebuilt-ui), [React](https://github.com/daily-demos/call-object-react), [Next.js](https://github.com/daily-demos/examples), [React Native](https://github.com/daily-demos/party-line), [and more](https://github.com/daily-demos) to help cover as many use cases as possible. 

Recently, we decided to expand our demo coverage even more with one of our favourite frameworks: [Vue](https://vuejs.org/)! 

In today’s tutorial, we’ll cover how to incorporate [Daily Prebuilt](https://www.daily.co/prebuilt) into your Vue app, as well as how to programmatically manage Daily Prebuilt controls via your app’s UI with our latest [demo app](https://github.com/daily-demos/vue-daily-prebuilt).

![Vue Daily Prebuilt demo home page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/laor4optd83k4ryqu7ni.png)

If you are interested in building a custom video chat app with Vue, fear not; we have an upcoming tutorial series on how to do just that. Stay tuned! 👀

---

## Tutorial requirements

Before we get started, be sure to [sign up for a Daily account](https://dashboard.daily.co/signup). Once you’re logged in, you can either [create a room](https://dashboard.daily.co/rooms/create) through the Dashboard or through the [REST API](https://docs.daily.co/reference#create-room).

For this tutorial, you can clone the [Daily Prebuilt Vue demo repo](https://github.com/daily-demos/vue-daily-prebuilt) and run it locally, or start from scratch and follow along as we build our Vue components.

To run the Daily Prebuilt Vue demo app locally, clone it and run the following in your terminal:

```bash
npm install
npm run serve
```

To view the app, open `http://localhost:8080` in the browser of your choice.

## Create a new Vue app

If you prefer to create your own Vue app to add Daily Prebuilt to, start by installing the [Vue CLI](https://cli.vuejs.org/guide/installation.html) globally to your machine. 

```bash
npm install -g @vue/cli
```

Once installed, we can [create a new Vue app](https://cli.vuejs.org/guide/creating-a-project.html#vue-create) to add Daily Prebuilt to using the Vue CLI.

In your terminal, run:

```bash
vue create daily-prebuilt-demo
```

Once the project is created, go to the project’s root directory and add [`daily-js`](https://docs.daily.co/reference#using-the-dailyco-front-end-library) as a dependency.


```bash
npm install @daily-co/daily-js 
```

Then, following the same instructions as above for the demo app, start the server:

```bash
npm run serve
```

## Demo project overview

 The [Daily Prebuilt Vue demo](https://github.com/daily-demos/vue-daily-prebuilt) only has four components:
1. `App.vue`, the parent component for every other component included in the app.
2. `Header.vue`, a completely optional component we included for the app’s title and project links.
3. `Home.vue`, the main component which is where Daily Prebuilt is embedded and the control panel is added when in a Daily call.
4. `Controls.vue`, the control panel for programmatically controlling Daily Prebuilt. This is also optional but useful for understanding how to interact with `daily-js` to customize your app’s usage of Daily Prebuilt.

We won’t go into the details of what’s happening in the `Header` since it’s static content, but what’s important to know is that the `App` component imports the `Header` and `Home` component, and both are displayed at all times.


```javascript
<template>
 <Header />
 <Home />
</template>
 
<script>
import Home from "./components/Home.vue";
import Header from "./components/Header.vue";
 
export default {
 name: "App",
 components: {
   Home,
   Header,
 },
};
</script>
```

## Ready to go Home: Importing Daily Prebuilt to your Vue app

The `Home` component is the most important one in this demo because it loads all the main content, including the Daily call and control panel. 

The default view of the Home component will include two buttons and an input:
1. The first button is only used if you’ve deployed the app via Netlify, so we’ll skip that for now. (Check out the project’s [README](https://github.com/daily-demos/vue-daily-prebuilt/blob/main/README.md) for more information.)
2. The input and second button are used to submit the Daily room URL you’ll be joining (i.e. from the Daily room created above). The format of this URL is `https://YOUR_DAILY_DOMAIN.daily.co/ROOM_NAME`. 

The container for this default home view is conditionally rendered depending on the `status` value in the component’s data option. 



```javascript
<div class="home" v-if="status === 'home'">
 …
</div>

```

The status can be `home`, `lobby`, or `call`. `home` refers to the default view, before a call has been started, and `lobby` refers to when a call has been started but not joined yet. (We call this the “hair check” view sometimes too, so you can view yourself and set up your devices before joining a call.) Lastly, `call` refers to when you are live in a Daily call. We’ll look at how the `status` value gets updated in a bit. 

There is also a call container `div` that is included in the `Home` component, which is conditionally displayed depending on the app’s current status. This means it is in the DOM in the default view but only visible to the user once a call has been started.

![The demo join button enables when a valid Daily room URL is entered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/47a6ybb21c3iue9pga6w.png)

Let’s look at the Vue template for how this is set up:

```javascript
<template>
 <main class="wrapper">
   <div class="home" v-if="status === 'home'">
     <h2>Daily Prebuilt demo</h2>
     <p>Start demo with a new unique room or paste in your own room URL</p>
     <div class="start-call-container">
       <button @click="createAndJoinRoom" :disabled="runningLocally">
         Create room and start
       </button>
       <p v-if="roomError" class="error">Room could not be created</p>
       <p class="subtext">or</p>
       <!-- Daily room URL is entered here -->
       <input
         type="text"
         placeholder="Enter room URL..."
         v-model="roomUrl"
         pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
         @input="validateInput"
       />
       <!-- button to submit URL and join call -->
       <button @click="submitJoinRoom" :disabled="!validRoomURL">
         Join room
       </button>
     </div>
   </div>
 
   <div class="call-container" :class="{ hidden: status === 'home' }">
     <!-- The Daily Prebuilt iframe is embedded in the div below using the ref -->
     <div id="call" ref="callRef"></div>
     <!-- Only show the control panel if a call is live -->
     <controls
       v-if="status === 'call'"
       :roomUrl="roomUrl"
       :callFrame="callFrame"
     />
   </div>
 </main>
</template>
```
Now that we know how the `Home` component is structured, let’s look at the JavaScript code that gives it functionality:


```javascript
<script>
import DailyIframe from "@daily-co/daily-js";
import Controls from "./Controls.vue";
import api from "../api.js";
 
export default {
 components: { Controls },
 name: "Home",
 data() {
   return {
     roomUrl: "",
     status: "home",
     callFrame: null,
     validRoomURL: false,
     roomError: false,
     runningLocally: false,
   };
 },
 created() {
   if (window?.location?.origin.includes("localhost")) {
     this.runningLocally = true;
   }
 },
 methods: {
   createAndJoinRoom() {
     api
       .createRoom()
       .then((room) => {
         this.roomUrl = room.url;
         this.joinRoom(room.url);
       })
       .catch((e) => {
         console.log(e);
         this.roomError = true;
       });
   },
   // Daily callframe created and joined below
   joinRoom(url) {
     if (this.callFrame) {
       this.callFrame.destroy();
     }
 
     // Daily event callbacks
     const logEvent = (ev) => console.log(ev);
     const goToLobby = () => (this.status = "lobby");
     const goToCall = () => (this.status = "call");
     const leaveCall = () => {
       if (this.callFrame) {
         this.status = "home";
         this.callFrame.destroy();
       }
     };
     // DailyIframe container element
     const callWrapper = this.$refs.callRef;
 
     // Create Daily call
     const callFrame = DailyIframe.createFrame(callWrapper, {
       iframeStyle: {
         height: "auto",
         width: "100%",
         aspectRatio: 16 / 9,
         minWidth: "400px",
         maxWidth: "920px",
         border: "1px solid var(--grey)",
         borderRadius: "4px",
       },
       showLeaveButton: true,
     });
     this.callFrame = callFrame;
 
     // Add event listeners and join call
     callFrame
       .on("loaded", logEvent)
       .on("started-camera", logEvent)
       .on("camera-error", logEvent)
       .on("joining-meeting", goToLobby)
       .on("joined-meeting", goToCall)
       .on("left-meeting", leaveCall);
 
     callFrame.join({ url });
   },
   submitJoinRoom() {
     this.joinRoom(this.roomUrl);
   },
   validateInput(e) {
     this.validRoomURL = !!this.roomUrl && e.target.checkValidity();
   },
 },
};
</script>

```

Let’s start by focusing on the `joinRoom` method, which is where all the Daily video call ✨magic✨happens. 


```javascript
joinRoom(url) {
  if (this.callFrame) {
    this.callFrame.destroy();
  }
  ...
```
First, if there is already a `callFrame` (i.e. the video call iframe), we destroy it to avoid multiple calls being loaded unintentionally. Defensive coding FTW. 💅


```javascript
// Daily event callbacks
const logEvent = (ev) => console.log(ev);
const goToLobby = () => (this.status = "lobby");
const goToCall = () => (this.status = "call");
const leaveCall = () => {
   if (this.callFrame) {
      this.status = "home";
      this.callFrame.destroy();
   }
};

```

Next, we set up the callbacks that will be used by `daily-js` whenever an event happens in the call that will affect our app’s UI. This can be moved outside the `joinRoom` function too, but we won’t worry about optimizing for now.

These callbacks are where we update our data options’ `status` value to know what stage of the call we’re in.

`const callWrapper = this.$refs.callRef;`

Next, we select the `div` container that we’ll instruct `daily-js` to embed the video call iframe into (the `DailyIframe` instance). 

`<div id="call" ref="callRef"></div>`

If we look back at the DOM structure, there was a `div` included with a ref added to it to simplify selecting that `div` in our `joinRoom` method. This is what we're targeting with `const callWrapper = this.$refs.callRef;`


```javascript
// Create Daily call
const callFrame = DailyIframe.createFrame(callWrapper, {
  iframeStyle: {
      height: "auto",
      width: "100%",
      aspectRatio: 16 / 9,
      minWidth: "400px",
      maxWidth: "920px",
      border: "1px solid var(--grey)",
      borderRadius: "4px",
  },
  showLeaveButton: true,
});
 
this.callFrame = callFrame;
```

Getting back to `joinRoom`, we then actually create the `DailyIframe` that will host our video call and assign it to the variable `callFrame`. This variable then gets assigned to our data option so it can be referenced later. (If you were using a state management library, you would add it to your app’s state at this point.)

*Note: The options passed to `createFrame`, like `iframeStyle`, are optional.*

```javascript
// Add event listeners and join call
callFrame
  .on("loaded", logEvent)
  .on("started-camera", logEvent)
  .on("camera-error", logEvent)
  .on("joining-meeting", goToLobby)
  .on("joined-meeting", goToCall)
  .on("left-meeting", leaveCall);
 
callFrame.join({ url });

```

Once the `callFrame` exists, we can attach all the Daily event listeners to it with our callbacks created earlier, and join the call. To join, make sure you pass the Daily room URL, which is the value the user entered into the input. 

After the `join` method is called, you should see two possible views depending on your room’s `prejoin UI` settings.

![Prejoin UI setting in Daily Dashboard](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tpmdk5fb50kebgc9gf9p.png)

If you have the `prejoin UI` option enabled, you will see the lobby view. The `joining-meeting` event will get triggered, which will call the `goToLobby` callback that we set above. 

![Prejoin UI in Daily Prebuilt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/caouwf8kjihk47kcpdw9.png)

In the lobby view, you will no longer see the default view because the `status` value has changed to `lobby`. If we review our DOM elements, we can see the call container now shows because `status !== ‘home’` (it equals `lobby` now). The controls do not show yet, though, because we’re not officially in the call yet.

```javascript
<div class="call-container" :class="{ hidden: status === 'home' }">
     <!-- The Daily Prebuilt iframe is embedded in the div below using the ref -->
     <div id="call" ref="callRef"></div>
     <!-- Only show the control panel if a call is live -->
     <controls
       v-if="status === 'call'"
       :roomUrl="roomUrl"
       :callFrame="callFrame"
     />
</div>
```

The second possible view, if you have the `prejoin UI` disabled for the room you’re in, is seeing the call view. This means you are in the Daily call! 💪

The `joined-meeting` event would have been triggered, calling the `goToCall` callback we set, which will update the `status` to be `call`. This status change will cause the controls to now show. 

![Daily Prebuilt call UI embedded in Vue app](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bqib6dduuzdu9me6fks.png)

--- 

## Controlling your in-call experience programmatically

One of the best things about Daily Prebuilt is that the hard parts of building video calls are done for you but there are still lots of options that can be configured or customized. 

Once the `DailyIframe` instance (our video call iframe) has been created, you have access to dozens of [instance methods](https://docs.daily.co/reference#instance-methods) to help you manage your call functionality. 

For example, let’s say you want to add a button to your app to leave a call. You can create a button that calls the [`.leave()` instance method](https://docs.daily.co/reference#%EF%B8%8F-leave) on click. 

To look at how some of these methods work, we can review how the `Controls` component is set up. 

To start, let’s see which props are passed to the `Controls` component where it’s used in `Home`. 


```javascript
<controls
   v-if="status === 'call'"
   :roomUrl="roomUrl"
   :callFrame="callFrame"
/>
```

The `v-if` means the controls are only rendered if the `status` value is equal to `call`. This means it only shows when a person is live in a call in this demo. 

The `roomUrl` prop is the URL the user submitted in the default home view. 

The `callFrame` prop is the DailyIframe instance created for the call, which gives us access to all the instance methods.

*Note: Not all instance methods are available for Daily Prebuilt. Refer to our [documentation](https://docs.daily.co/reference#instance-methods) to know which ones can be used.*

Now let’s take a look at our `Controls` component and see how the HTML is structured:


```javascript
<template>
 <div class="controls">
   <h2>Call overview</h2>
   <hr />
   <h3>Invite participants</h3>
   <label for="urlInput">Share URL below to invite others</label>
   <div>
<!-- Room URL to copy and share -->
     <input type="text" id="urlInput" :value="roomUrl" />
     <button @click="copyUrl" class="teal">{{ copyButtonText }}</button>
   </div>
   <hr />
   <h3>Example custom controls</h3>
   <p>
     You can also create your own meeting controls using daily-js methods
   </p>
   <div>
     <button @click="toggleCamera">Toggle camera</button>
     <button @click="toggleMic">Toggle mic</button>
     <button @click="toggleScreenShare">Toggle screen share</button>
     <button @click="expandFullscreen">Expand fullscreen</button>
     <button @click="toggleLocalVideo">
       {{ localVideoText }} local video
     </button>
     <button @click="toggleRemoteParticipants">
       {{ remoteVideoText }} remote participants (Speaker view only)
     </button>
     <button @click="leaveCall">
       Leave call
     </button>
   </div>
 </div>
</template>

```

We display the `roomUrl` prop in the input for the user to copy and share with others so they can join the call, too.

We also have eight buttons included in the control panel to programmatically interact with the `DailyIframe` instance. There interactions include:
- Turning the local camera on and off
- Turning the local microphone on an off
- Sharing the local call participant’s screen
- Expanding Daily Prebuilt to be fullscreen
- Hiding and showing the local participant’s tile in the call
- Hiding and showing the participants bar, which is where all the remote participants’ tiles are locally while in speaker mode
- Leaving the call to go back to the default home view

### Toggle your local camera programmatically

To understand how these work, let’s review a couple, starting with toggling the local camera.

`<button @click="toggleCamera">Toggle camera</button>`

To turn the local camera on and off, the control panel button has the following click event attached to it:


```javascript
toggleCamera() {
  this.callFrame.setLocalVideo(!this.callFrame.localVideo());
},

```

`this.callFrame` refers to the `callFrame` prop passed in the `Home` component, which gives us access to the `DailyIframe` instance. We can then call [`.setLocalVideo()`](https://docs.daily.co/reference#%EF%B8%8F-setlocalvideo), an instance method that accepts a boolean value. 

The current status of the local camera can be accessed with the `.localVideo()` instance method, which will return whether the local camera is currently on or off. Since we want this method to toggle the current state, we can pass `.setLocalVideo()` whatever the inverse of the current state of the camera is with `!this.callFrame.localVideo()`.

So, if the camera is currently on, calling `this.callFrame.setLocalVideo(!this.callFrame.localVideo());` is the same as calling `this.callFrame.setLocalVideo(false);` to turn it off.

### Go fullscreen with the click of a button ✨

The other buttons in the control panel mostly work the same way. Let’s take a look at one more example to see how to update your Daily Prebuilt calls programmatically.

The control panel includes a button to make the Daily Prebuilt iframe fullscreen:

`<button @click="expandFullscreen">Expand fullscreen</button>`

The click handler on this button uses the `callFrame` prop to access the `DailyIframe` instance, which can then call the [`requestFullscreen()`](https://docs.daily.co/reference#requestfullscreen) instance method.

![Click fullscreen button to make video call fullscreen](https://www.daily.co/blog/content/images/2021/08/CleanShot-2021-08-17-at-13.59.02.gif)

And with one click, you're in fullscreen mode. It’s really as simple as that! 🙌

---

## Wrapping up

Now that you know how to embed [Daily Prebuilt](https://www.daily.co/prebuilt) in a Vue app, you can add Daily video chat to any Vue projects you’re building! Tag us on Twitter ([@trydaily](https://twitter.com/trydaily)) to show us your projects. 😊

In terms of next steps, to learn how to customize your video apps even more, try updating your [Daily Prebuilt colour theme](https://www.daily.co/blog/introducing-color-theming-for-embeddable-video-calls-a-new-feature-for-daily-prebuilt/).