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.
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:
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:
So, let's start by installing 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.
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.
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.