MadMikeyB

Posted 7 months ago

How to create a dynamic, re-usable follow button with VueJS, Laravel and Watchable

View Profile

Recently I had a need to create a follow button. The client had three distinct requests; no page refresh, re-usable for any content type, and dynamic. Naturally I reached for VueJS, and I thought I'd share with you how I did it.


Pre Requisites:

TL;DR? Repository is here: https://github.com/madmikeyb/follow-button


Getting Started.

First things first, you need to get Laravel installed - via composer create project, or the laravel new installer command. Once that is done ensure that vue is installed by running npm install.

Let's set up authentication by running the following command:

php artisan make:auth && php artisan migrate

Next up, we need to require the watchable trait from composer. You can do this by running:

composer require jamesmills/watchable

Once you've done this, you need to publish the assets from the trait and run the database migrations:

php artisan vendor:publish --provider="JamesMills\Watchable\WatchableServiceProvider" --tag="migrations" && php artisan migrate

Finally, you need to visit the project in your browser. Either by using php artisan serve, Valet, Homestead or MAMP, to ensure everything is working.


Dummy Data.

You need to create yourself a user by registering using the authentication methods you created earlier. Go to your-project.test/register and create an account.

Now we need some dummy data, so let's create a dummy articles table and a model factory.

php artisan make:model Article -m && php artisan make:factory ArticleFactory

Inside xxxx_xx_xx_create_articles_table.php you need to define your table columns, here is what I used:

Schema::create('articles', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id');
$table->string('title');
$table->string('slug');
$table->longtext('body');
$table->timestamps();
});

Now we need some dummy articles. Let's use our ArticleFactory.php file we just created. Here is what I used:

$factory->define(App\Article::class, function (Faker $faker) {
return [
'title' => $faker->sentence,
'slug' => str_slug($faker->sentence),
'body' => $faker->paragraph,
'user_id' => factory('App\User')->create()->id
];
});

Finally, boot up php artisan tinker, and run the following:

$articles = factory('App\Article', 50)->create();

Now we have some dummy articles, let's move on to displaying them!


Initialising our Routes.

Inside /routes/web.php file we can now expose our articles to the included welcome view template. A quick and easy way of doing this is as follows:

Route::get('/', function () {
$article = App\Article::first();
return view('welcome', compact('article'));
});

Route::get('/{article}', function ($article) {
$article = App\Article::find($article);
return view('welcome', compact('article'));
});

Later on we may want to move this to a controller, but for now it is perfectly acceptable.


Setting up the Views.

Next up, let's strip out the stuff we don't need inside /resources/views/welcome.blade.php so that we end up with something a bit more like this:

@extends('layouts.app')
@section('content')
<div class="container">
<div class="page-header">
<h1>Articles <small>&mdash; {{$article->title}}</small></h1>
</div>
<div class="row">
<div class="col-md-3">
<div class="panel panel-default">
<div class="panel-body">
<follow-button id="{{$article->id}}" type="article"></follow-button>
</div>
</div>
</div>
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{$article->title}}</h3>
</div>
<div class="panel-body">
{{$article->body}}
</div>
</div>
</div>
</div>
</div>
@endsection


This may look like a lot, but what we're now going to focus on is the following element:

<follow-button id="{{$article->id}}" type="article"></follow-button>
This is our custom Vue component. You'll see we accept two props, id and type. If you load your test site in the browser now and open up your console, you'll see a console error which informs you that follow-button is not a recognised component.

Ensure npm run watch is now running before proceeding!

Registering the Component

So we need to register our Vue component, but wait! We haven't even created it yet. Let's fix that by creating FollowButton.vue inside /resources/assets/js/components/FollowButton.vue with the following code:

<template>
<div>
<a class="btn btn-primary btn-block">
<i class="fa fa-bell"></i> Follow
</a>

<a class="btn btn-danger btn-block">
<i class="fa fa-bell-o"></i> Following
</a>
</div>
</template>

<script>
export default {
props: ['id', 'type'],
data() {
return {

}
},
methods: {

}
}
</script>

This is the basic make-up of a Vue Component, with a template tag and a script tag. All that's left to do is to register it in /resources/assets/js/app.js like so:

Vue.component('follow-button', require('./components/FollowButton.vue'));
I recommend doing this below Laravel's default example component line, above:

const app = new Vue({
el: '#app'
});

Right now all we're outputting is a follow and unfollow button, which when clicked don't actually do anything. Let's fix that.


Toggling Follow / Unfollow

Starting small, we can simply toggle the follow / unfollow button by adding a variable into our data() object:

data() {
return {
isFollowing: false
}
},

Then, we can actually toggle this boolean by adding a 'toggleFollow()' method into our methods section:

methods: {
toggleFollow() {
this.isFollowing = !this.isFollowing
}
}


Finally to tie it all together, inside our template element we should check if the isFollowing variable is true or false using v-if and v-else.


<template>
<div>
<span v-if="!isFollowing">
<a class="btn btn-primary btn-block" @click="toggleFollow()">
<i class="fa fa-bell"></i> Follow
</a>
</span>
<span v-else>
<a class="btn btn-danger btn-block" @click="toggleFollow()">
<i class="fa fa-bell-o"></i> Following
</a>
</span>
</div>
</template>


Note that we utilise a @click property on each button to simulate a button press and trigger the toggleFollow() method we just added. If everything went well, you should now have a button which you can click and unclick which toggles the Follow and Following buttons:



Remind me why I didn't do this in jQuery?

This is great and everything but so far we've not done anything which we couldn't do with jQuery's .toggle() method. I truly believe that Vue's real power comes in it's reactivity when it comes to dealing with API calls. jQuery has $.ajax but the syntax is a little clunky and requires passing around of tokens with every call etc. 

Making a start on the API
 
Let's make a start on our API. Laravel and the Watchable trait which we required earlier now make this final part criminally easy. Using Laravel's artisan tool let's create a controller to handle all of our API calls, as well as adding the necessary routes which we need.

php artisan make:controller Api\\FollowController

Rather than build up our FollowController gradually, I'm going to paste it in it's entirety and let the docblocks explain.

<?php

namespace App\Http\Controllers\Api;

use App\Article;
use App\Video;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class FollowController extends Controller
{
/**
* Toggle the follow state for the given content item.
*
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\Response
*/
public function toggleFollow(Request $request)
{
$item = $this->getItem($request);

$item->toggleWatch();

return response()->json(['success' => true], 200);
}

/**
* Check if the user is already following the given content item
*
* @param Illuminate\Http\Request $request
* @return Illuminate\Http\Response
*/
public function following(Request $request)
{
$item = $this->getItem($request);

return response()->json($item->isWatched());
}

/**
* Fetch the given item from the database
*
* @param Illuminate\Http\Request $request
* @return Illuminate\Database\Eloquent\Model|Illuminate\Http\Response
*/
protected function getItem(Request $request)
{
$model = $this->getModel($request->type);

if (!$model) {
return response()->json(['success' => false, 'message' => 'The specified type could not be converted to a model instance.'], 404);
}

return $model::find($request->id);
}

/**
* Determine the model for the given content type
*
* @param string $type The content type
* @return Illuminate\Database\Eloquent\Model|bool
*/
protected function getModel($type)
{
switch ($type) {
case 'article':
return Article::class;
break;

case 'video':
return Video::class;
break;

default:
return false;
break;
}
}
}

This is quite a simple class, some methods could be extracted into something which isn't the controller, but it works for a first pass until refactoring is required.

Explained in brief, the toggleFollow method simply defers to a getItem method which does the heavy lifting of determining the content type (Article, Video, Post, Profile, Comment, etc) from the getModel method, verifying the response and returning the requested item using the Eloquent find method.

Next we need to add our Routes for our Vue Component to be able to talk to our API. Inside /routes/web.php add the following:

Route::get('/api/toggleFollow', 'Api\\FollowController@toggleFollow');
Route::get('/api/following', 'Api\\FollowController@following');

Linking up our Vue Component and our API.

All that is left is to link up our Vue Component to our API. I will post the fully completed Vue component below and then explain it:

<template>
<div>
<span v-if="!isFollowing">
<a class="btn btn-primary btn-block" @click="toggleFollow()">
<i class="fa fa-bell"></i> Follow
</a>
</span>
<span v-else>
<a class="btn btn-danger btn-block" @click="toggleFollow()">
<i class="fa fa-bell-o"></i> Following
</a>
</span>
</div>
</template>

<script>
export default {
props: ['id', 'type'],
data() {
return {
isFollowing: this.userIsFollowing()
}
},
methods: {
toggleFollow() {
axios.get('/api/toggleFollow?id=' + this.id + '&type=' + this.type)
.then(({data}) => {
this.isFollowing = !this.isFollowing
});
},
userIsFollowing() {
axios.get('/api/following?id=' + this.id + '&type=' + this.type)
.then(({data}) => {
if (data == 1) {
this.isFollowing = true;
} else {
this.isFollowing = false;
}
});
}
}
}
</script>

Breakdown.

Starting at the top, we have our template which simply checks whether or not the user is currently following the currently requested item.

The isFollowing variable is controlled by the brand new userIsFollowing() method which in turn talks to the following() method on Api\FollowController. This controls which follow button shows by returning a boolean of true or false.

Each follow button has a different icon and different text, but once clicked they hit the toggleFollow() method which in turn talks to the toggleFollow() method on Api\FollowController.

This then updates the isFollowing variable with the inverse of what it currently is. So if the isFollowing variable is currently false, the user clicks the button, they must be requesting to follow, so the variable becomes the inverse of false - true and as such Vue reactively updates the text to "Following" to indicate an action has taken place.

Re-usability

The beauty of all this is really in our API and implementation:

  1. Because we're using Watchable which is polymorphic, we're able to enable following functionality on any number of models.
  2. Because our API doesn't care about which Model we're using - we can simply change our type property when adding a new follow button element to enable the follow button component on any content type which supports Watchable.


<follow-button id="{{$article->id}}" type="article"></follow-button>
<follow-button id="{{$profile->id}}" type="profile"></follow-button>
<follow-button id="{{$video->id}}" type="video"></follow-button>
<follow-button id="{{$tutorial->id}}" type="tutorial"></follow-button>
<follow-button id="{{$product->id}}" type="product"></follow-button>
<follow-button id="{{$order->id}}" type="order"></follow-button>
<follow-button id="{{$book->id}}" type="book"></follow-button>

Adding a new content type to the follow button is as easy as making the custom element, and adding the content type into the Api\FollowController::findModel class map.

We don't even have to worry about our route key name being different from ID (for example, if articles is using slugs or UUID's for their URL's), as Eloquent's find method only searches the primary key. :D

Conclusion

I hope this tutorial has been relatively easy to follow along, I tried to be as detailed as possible without going too deep and getting boring!

Let me know if it helped or if you have any questions or comments, I am @MadMikeyB on Twitter, or leave a comment below!

Thanks for reading!
Must be logged in to comment
How to create a dynamic, re-usable follow button with VueJS, Laravel and Watchable / Welcome to Signl