Back to Course |
Laravel Reverb: Four "Live" Practical Examples

Who's Online: User X is Viewing the Page

Reverb has many potential use cases, one of them being presence channels. They allow us to see who's online in real time. For example, we can show a list of people viewing the page/document:

This tutorial will take a simple "Document view" page and add a list of users viewing the page, updating it in real-time whenever someone joins/leaves the page. We'll use Reverb's presence channels to do this.


Project

Our current project is really simple. We have a Document model and a DocumentController with a show() method. This method returns a view with the document's content.

app/Http/Controllers/DocumentController.php

public function show(Document $document)
{
return view('documents.show', compact('document'));
}

This returns a simple view with the document's content:

resources/views/documents/show.blade.php

<x-app-layout>
{{-- ... --}}
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="p-6 sm:px-20 bg-white border-b border-gray-200">
<div class="text-2xl">
{{ $document->title }}
</div>
<div class="mt-6 text-gray-500">
{{ $document->description }}
</div>
</div>
</div>
</div>
</div>
</x-app-layout>

This gives us a very simple page with the document's title and description:


Overview

We want to add a list of users currently viewing the document. This list needs to be updated in real-time as people enter and leave the page. Here's exactly what we want:

  • Add Echo presence channel to our document
  • On page load - create a list of people viewing the document from an HTML template (avatar and name)
  • When on a page and someone else enters - add them to the list automatically
  • When on a page and someone else leaves - remove them from the list automatically

So, let's start by installing Reverb.


Install and Run the Reverb

To install Reverb, we have to call this command:

php artisan install:broadcasting

This will ask us if we want to install Reverb, enter yes and press Enter.

Once the package installs - it will ask if we want Node dependencies to be installed. Enter yes and press Enter.

That's it! Reverb is installed and ready to be used. No more configuration is needed at this point.


Adding Real-Time Presence Channels

Next, we need to implement channels in our application. This is done by adding a channel to our channel file:

routes/channels.php

use Illuminate\Support\Facades\Storage;
 
// ...
 
Broadcast::channel('App.Modes.Document.{id}', function ($user, $id) {
return ['id' => $user->id, 'name' => $user->name, 'avatar' => Storage::url($user->avatar)];
});

What this does is create a channel for each document. This channel then adds users to the channel with their ID, name, and avatar. We'll use this to create our list of users viewing the document.

Next, we need to add the channel to our document view:

resources/views/documents/show.blade.php

{{-- ... --}}
<div class="w-full mb-4 pb-4 border-b-2">
<div>
<h2 class="text-xl">Users viewing:</h2>
</div>
<div id="documentViewing" class="w-full"></div>
</div>
<div class="text-2xl">
{{ $document->title }}
</div>
<div class="mt-6 text-gray-500">
{{ $document->description }}
</div>
{{-- ... --}}

With this, we have added a container (div) where we will push the list of users. Now, we need to configure our Echo to listen to the channel and update the list:

resources/views/documents/show.blade.php

{{-- ... --}}
 
<script>
window.addEventListener('DOMContentLoaded', function () {
window.Echo.join('App.Modes.Document.{{ $document->id }}')
.here((users) => {
// Load current users
})
.joining((user) => {
// Adds new joined users to the list
})
.leaving((user) => {
// Remove users that left the document
});
});
</script>
</x-app-layout>

This script will wait for our page to load and join the channel. Once joined, it will do the following:

  • here - This method returns all users in the channel. We can use this to display the list of users when the page loads.
  • joining - This method is called when a new user joins the channel. We can use this to add the user to the list.
  • leaving - This method is called when a user leaves the channel. We can use this to remove the user from the list.

But what will we add? Well, we need a template! So, let's create one:

resources/views/documents/show.blade.php

{{-- ... --}}
 
<script type="text/html" id="live-user-template">
<span id="live-user-_ID_" class="inline-block">
<img src="_AVATAR_" alt="_NAME_" class="w-8 h-8 inline-block"/> _NAME_
</span>
</script>

This template will act as our user list item. We will replace _ID_, _AVATAR_ and _NAME_ with the user's id, avatar and name respectively.

Note: We use a script tag with type="text/html" to store our template. This can be replaced with a hidden div or a variable, but we feel this is the cleanest way to store the template.

Last, we need to add the logic to the methods:

Let's start by adding the logic to the here method. We will loop through all users and add them to the list:

resources/views/documents/show.blade.php

{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
window.Echo.join('App.Modes.Document.{{ $document->id }}')
.here((users) => {
// Load current users
// Add new users to the list
users.forEach(user => {
let html = document.querySelector('#live-user-template').innerHTML;
html = html.replace(/_ID_/g, user.id);
html = html.replace(/_AVATAR_/g, user.avatar);
html = html.replace(/_NAME_/g, user.name);
document.getElementById('documentViewing').innerHTML += html;
});
})
{{-- ... --}}
});

Then, we can handle the joining method. This will add the user to the list:

resources/views/documents/show.blade.php

{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
window.Echo.join('App.Modes.Document.{{ $document->id }}')
{{-- ... --}}
.joining((user) => {
// Load current users
// We don't want to add the user if it's already there
if (document.getElementById('live-user-' + user.id)) return;
 
// Add new users to the list
let html = document.querySelector('#live-user-template').innerHTML;
html = html.replace(/_ID_/g, user.id);
html = html.replace(/_AVATAR_/g, user.avatar);
html = html.replace(/_NAME_/g, user.name);
document.getElementById('documentViewing').innerHTML += html;
})
{{-- ... --}}
});

And lastly, we need to handle the leaving method. This will remove the user from the list:

resources/views/documents/show.blade.php

{{-- ... --}}
 
window.addEventListener('DOMContentLoaded', function () {
window.Echo.join('App.Modes.Document.{{ $document->id }}')
{{-- ... --}}
.leaving((user) => {
// Remove users that left the document
document.getElementById('live-user-' + user.id).remove();
});
});

And our final file will look like this:

resources/views/documents/show.blade.php

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Document') }}
</h2>
</x-slot>
 
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="p-6 sm:px-20 bg-white border-b border-gray-200">
<div class="w-full mb-4 pb-4 border-b-2">
<div>
<h2 class="text-xl">Users viewing:</h2>
</div>
<div id="documentViewing" class="w-full"></div>
</div>
<div class="text-2xl">
{{ $document->title }}
</div>
<div class="mt-6 text-gray-500">
{{ $document->description }}
</div>
</div>
</div>
</div>
</div>
 
<script type="text/html" id="live-user-template">
<span id="live-user-_ID_" class="inline-block">
<img src="_AVATAR_" alt="_NAME_" class="w-8 h-8 inline-block"/> _NAME_
</span>
</script>
 
<script>
window.addEventListener('DOMContentLoaded', function () {
window.Echo.join('App.Modes.Document.{{ $document->id }}')
.here((users) => {
// Add new users to the list
users.forEach(user => {
let html = document.querySelector('#live-user-template').innerHTML;
html = html.replace(/_ID_/g, user.id);
html = html.replace(/_AVATAR_/g, user.avatar);
html = html.replace(/_NAME_/g, user.name);
document.getElementById('documentViewing').innerHTML += html;
});
})
.joining((user) => {
// We don't want to add the user if it's already there
if (document.getElementById('live-user-' + user.id)) return;
 
// Add new users to the list
let html = document.querySelector('#live-user-template').innerHTML;
html = html.replace(/_ID_/g, user.id);
html = html.replace(/_AVATAR_/g, user.avatar);
html = html.replace(/_NAME_/g, user.name);
document.getElementById('documentViewing').innerHTML += html;
})
.leaving((user) => {
// Remove users that left the document
document.getElementById('live-user-' + user.id).remove();
});
});
</script>
</x-app-layout>

That's it! We are done with the list and can move into testing.


Testing

To test this, we need to make sure to run two things:

First, we must run npm run build to build our assets.

Then, we need to start our Reverb server:

php artisan reverb:start

Once that is done, we can open two browsers and navigate to the document page. We should see the list of users on both pages. If we refresh one of the pages, we should see the list update in real time.


You can find the complete code for this tutorial here.