Implement a Favoriting Feature Using Laravel and Vue.js

Chimezie Enyinnaya

These days, various web applications are in one way or the other implementing a kind of favorite/like/recommend feature on the websites. These can be seen on sites like Medium, Facebook, Laracasts, and even here on scotch.io and school.scotch.io.

In this tutorial, I'll be showing you how to implement a favorites feature using Vue.js in your Laravel application. Though we'll be using the term favorites in this tutorial, this can be replaced with likes, recommends depending on your application.

What We'll Be Building

We'll be building a simple Posts app. This app will comprise of users and posts. Users will be able to create posts and as well mark posts as favorites. Finally, users will be able to see all the posts they marked as favorites.

The app will have a User model and a Post model, there will be an authentication system which will allow only authenticated users mark/unmark posts as favorites. We'll make use of VueJs and Axios to make marking/un-marking posts as favorites dynamic, that is without reloading the page.

Before we start building, let's take a quick look at what the Posts app will look like when we are done.

Let's Get started

We'll start by creating a new Laravel project, the name of the project will be laravel-vue-favorite.

laravel new laravel-vue-favorite

This will create a new Laravel 5.4 (which is the current version as at the time of this tutorial) project.

Installing NPM Dependencies

In a fresh installation of Laravel, Laravel provides some frontend frameworks and libraries with some basic setup to integrate these packages together. Among the frameworks and libraries are Bootstrap, VueJs and Axios, which we will be using in this tutorial. But we still need to install these dependencies through NPM:

npm install

Also, we'll make use of Laravel Mix to compile and build our CSS and JavaScript. The command above will also install all Laravel Mix dependencies.

Models And Migrations

For our Posts app, we'll need a User model (which comes with Laravel), a Post model and a Favorite model and their respective migration files.

php artisan make:model Post -m
php artisan make:model Favorite -m

These will create a Post model and a Favorite model along with their migration files respectively. Open the posts table migration file and update the up() with:

/**
 * Define posts table schema
 */
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id')->unsigned();
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

The posts table will contain an id, user_id (ID of the user that created the post), title, body, and some timestamps columns.

Next, open the favorites table migration file and update the up() with:

/**
 * Define favorites table schema
 */
public function up()
{
    Schema::create('favorites', function (Blueprint $table) {
        $table->increments('id');
        $table->integer('user_id')->unsigned();
        $table->integer('post_id')->unsigned();
        $table->timestamps();
    });
}

The favorites table will be a pivot table. It will contain two columns: user_id which will hold the ID of the user that favorited a post and post_id which will the ID of the post that was favorited.

For the users table migration, we'll be using the default Laravel provided.

Before we run our migrations, let's setup our database. Add your database details to the .env file:

DB_DATABASE=laravue
DB_USERNAME=root
DB_PASSWORD=root

Remember to update with your own database details. Now we can go on and run our migrations:

php artisan migrate

Database Seeder

We'll also generate some seed data which we can test our app with. To generate dummy data, we'll make use of Laravel Model Factories. Laravel model factories make use of the Faker PHP library.

We'll generate dummy data of Users and Posts. When you open the database/factories/ModelFactory.php file, you will see that Laravel provides a User model factory, which means we only need to create a Post model factory. Add the snippets below to database/factories/ModelFactory.php just after the User model factory:

// database/factories/ModelFactory.php

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    // Get a random user
    $user = \App\User::inRandomOrder()->first();

    // generate fake data for post
    return [
        'user_id' => $user->id,
        'title' => $faker->sentence,
        'body' => $faker->text,
    ];
});

Let's quickly run through the code. Remember from our posts table migration, we defined that a post must have a user ID. So, we get a random user and assign the user_id of a post to the ID of the random user, then we use Faker to generate the title and body of the post.

With our model factories done, let's move on to create our database seeder classes by running these commands:

php artisan make:seeder UsersTableSeeder
php artisan make:seeder PostsTableSeeder

Open database/seeds/UsersTableSeeder.php and update the run() with:

// database/seeds/UsersTableSeeder.php

/**
 * Run the database seeds to create users.
 *
 * @return void
 */
public function run()
{
    factory(App\User::class, 5)->create();
}

This will create 5 different users with dummy data when the seeder is run. We'll do the same for Posts. Open database/seeds/PostsTableSeeder.php and update the run() with:

// database/seeds/PostsTableSeeder.php

/**
 * Run the database seeds to create posts.
 *
 * @return void
 */
public function run()
{
    factory(App\Post::class, 10)->create();
}

This will create 10 different posts with dummy data when the seeder is run.

Before we run the database seeders, let's update the database/seeds/DatabaseSeeder.php which is provided by default:

// database/seeds/DatabaseSeeder.php

/**
 * Run the database seeds.
 *
 * @return void
 */
public function run()
{
    $this->call(UsersTableSeeder::class);
    $this->call(PostsTableSeeder::class);
}

Now we can run the database seeders:

php artisan db:seed

You should now see some dummy data in your database.

Authenticating Users

Before a user can mark a post has favorite, the user must be logged in. So we need a kind of authentication system. Luckily for us, Laravel got our back on this. We'll use the artisan make:auth command to scaffold an authentication system.

php artisan make:auth

This will create all of the necessary routes and views for authentication. We can go and register as a user, which we will use to test the functionality of the application we are building.

Defining Our Routes

Let's define the routes of our application. Open routes/web.php and update with below:

// routes/web.php

Auth::routes();

Route::get('/', 'PostsController@index');

Route::post('favorite/{post}', 'PostsController@favoritePost');
Route::post('unfavorite/{post}', 'PostsController@unFavoritePost');

Route::get('my_favorites', 'UsersController@myFavorites')->middleware('auth');

The routes are pretty straightforward. Auth routes that Laravel created when we ran the make:auth command. A route to the homepage that will list all posts, two other routes for favoriting and unfavoriting posts. Lastly, a route that displays all posts that have been marked as favorites by a user. This route will be accessible to only authenticated users.

When a user registers or login, Laravel will redirect them to the /home route by default. Since we have removed the /home route that Laravel created when we ran make:auth. We need to update the redirectTo property of both app/Http/Controllers/Auth/LoginController.php and app/Http/Controllers/Auth/RegisterController.php to:

protected $redirectTo = '/';

Defining Users To Favorite Posts Relationship

Since a user can mark many posts as favorites and a post can be marked as favorites by many users, the relationship between users and favorite posts will be a many to many relationships. To define this relationship, open the User model and add a favorites():


// app/User.php

/**
 * Get all of favorite posts for the user.
 */
public function favorites()
{
    return $this->belongsToMany(Post::class, 'favorites', 'user_id', 'post_id')->withTimeStamps();
}

Laravel will assume the pivot table is post_user but since we gave the pivot table a different name (favorites), we have to pass in some additional arguments. The second argument is the name of the pivot table (favorites). The third argument is the foreign key name (user_id) of the model on which you are defining the relationship (User), while the fourth argument is the foreign key name (post_id) of the model that you are joining to (Post).

Noticed we chained withTimeStamps() to the belongsToMany(). This will allow the timestamps (create_at and updated_at) columns on the pivot table be affected whenever a row is inserted or updated.

Posts Controller

Let's create a new controller that will handle displaying posts, marking a post as favorite and unfavorite a post.

php artisan make:controller PostsController

Open the newly created app/Http/Controllers/PostsController.php and add the snippet below to it:

// app/Http/Controllers/PostsController.php

// remember to use the Post model
use App\Post;

/**
 * Display a paginated list of posts.
 *
 * @return Response
 */
public function index()
{
    $posts = Post::paginate(5);

    return view('posts.index', compact('posts'));
}

The index() will get all posts and paginate them into 5 per page. Then render a view file (that we are yet to create) along with the posts, which will do the actual displaying of the posts.

Remember when we ran make:auth command that Laravel created some views. We'll be using the resources/views/layouts/app.blade.php that was created and make some few additions to it. Add the code below just after the <title>:

// resources/views/layouts/app.blade.php

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" />

Then add this just before the Logout list item:

// resources/views/layouts/app.blade.php

<li>
    <a href="{{ url('my_favorites') }}">My Favorites</a>
</li>

Now let's create the index view. Create a new posts folder within views directory and create a new index.blade.php file within the newly created folder. Paste the code below into resources/views/posts/index.blade.php:

// resources/views/posts/index.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="page-header">
                <h3>All Posts</h3>
            </div>
            @forelse ($posts as $post)
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {{ $post->title }}
                    </div>

                    <div class="panel-body">
                        {{ $post->body }}
                    </div>
                </div>
            @empty
                <p>No post created.</p>
            @endforelse

            {{ $posts->links() }}
        </div>
    </div>
</div>
@endsection

Pretty simple markup that displays a paginated list of posts. Open the homepage in your browser, you should see page like the image below:

Next, let's go back to PostsController and add the methods that will handle marking a post as favorite and unfavorite a post. Add the code below to PostsController:

// app/Http/Controllers/PostsController.php

// remember to use
use Illuminate\Support\Facades\Auth;

/**
 * Favorite a particular post
 *
 * @param  Post $post
 * @return Response
 */
public function favoritePost(Post $post)
{
    Auth::user()->favorites()->attach($post->id);

    return back();
}

/**
 * Unfavorite a particular post
 *
 * @param  Post $post
 * @return Response
 */
public function unFavoritePost(Post $post)
{
    Auth::user()->favorites()->detach($post->id);

    return back();
}

The favoritePost() takes a post as an argument. Using the favorites relationship we defined above, we attach the post ID to the ID of the authenticated user then insert into the favorites table. Finally, return back to the previous page.

The unFavoritePost() is the reverse of favoritePost() which simply remove the ID of the authenticated user along with the post ID from the favorites table.

Integrating With VueJs

It's time to integrate Vue into our application. We'll make the favorite button/icon a Vue component. Making it a Vue component will allow for reuse in multiple places with our application.

Once the favorite button/icon is clicked, we'll mark the post as favorite or unfavorite without reloading the page, that is through AJAX. For this, we'll make use of Axios which is a Promise based HTTP client for the browser and node.js.

Creating The Favorite Component

Create a new Favorite.vue file within the resources/assets/js/components folder and paste the code below into it:

// resources/assets/js/components/Favorite.vue

<template>
    <span>
        <a href="#" v-if="isFavorited" @click.prevent="unFavorite(post)">
            <i  class="fa fa-heart"></i>
        </a>
        <a href="#" v-else @click.prevent="favorite(post)">
            <i  class="fa fa-heart-o"></i>
        </a>
    </span>
</template>

<script>
    export default {
        props: ['post', 'favorited'],

        data: function() {
            return {
                isFavorited: '',
            }
        },

        mounted() {
            this.isFavorited = this.isFavorite ? true : false;
        },

        computed: {
            isFavorite() {
                return this.favorited;
            },
        },

        methods: {
            favorite(post) {
                axios.post('/favorite/'+post)
                    .then(response => this.isFavorited = true)
                    .catch(response => console.log(response.data));
            },

            unFavorite(post) {
                axios.post('/unfavorite/'+post)
                    .then(response => this.isFavorited = false)
                    .catch(response => console.log(response.data));
            }
        }
    }
</script>

The Favorite component has two sections: template and script. In the template section, we defined the markup that will be rendered when the component is used. We are using conditional rendering to show the appropriate button/icon. That is, if isFavorited is true, the button/icon should be marked as favorite and on click of the button/icon should trigger unFavorite(). Else the button/icon should be marked as unfavorite and on click of the button/icon should trigger favorite().

Moving on to the script section, we defined some properties for the component; post (will be the ID of the post) and favorited (will either be true or false depending on if the post has been favorited by the authenticated user). We also defined an isFavorited data which will be used for the conditional rendering from above.

When the component is mounted, we set the value of isFavorited to the value of isFavorite computed property. That is, the isFavorite computed property will return the value of favorited prop which will either be true or false. We use a computed property so as to reactively get the value of the favorited prop instead using the value of favorited prop that was passed directly.

Lastly, we defined two methods: favorite() and unFavorite() which both accepts the post prop as arguments. Using Axios, we make a POST request to the routes we defined earlier. For the favorite(), once the POST request is successful, we set isFavorited to true and otherwise console log the errors. Same is applicable to the unFavorite() just that we set isFavorited to false.

Registering The Favorite Component

Before we can start to use the Favorite component, we need to first register it on our Vue root instance. Open resources/assets/js/app.js, you will see that Laravel register an Example component. We are going to replace that with the Favorite component:

// resources/assets/js/app.js

Vue.component('favorite', require('./components/Favorite.vue'));

Now we can compile and build our styles and scripts:

npm run dev

Using The Favorite Component

We can now use the Favorite component. Open resources/views/posts/index.blade.php and add the snippets below to it after the closing div of the panel-body:

// resources/views/posts/index.blade.php

@if (Auth::check())
    <div class="panel-footer">
        <favorite
            :post={{ $post->id }}
            :favorited={{ $post->favorited() ? 'true' : 'false' }}
        ></favorite>
    </div>
@endif

The favorite button/icon will only be displayed to authenticated users. As you can see, we passed to the Favorite component the props that we defined when we created the component. To know if a post is has been favorited by the authenticated user, we call a favorited() (which we are yet to create) on the post.

To create favorited(), open app/Post.php and add the code below to it:

// app/Post.php

// remember to use
use App\Favorite;
use Illuminate\Support\Facades\Auth;

/**
 * Determine whether a post has been marked as favorite by a user.
 *
 * @return boolean
 */
public function favorited()
{
    return (bool) Favorite::where('user_id', Auth::id())
                        ->where('post_id', $this->id)
                        ->first();
}

This gets and casts to boolean the first result where the user_id is equal to that of the authenticated user and where the post_id is equal to the ID of the post the method is called on.

If you visit the homepage of the application in the browser and login, you should get something similar to:

As you can see I have marked some posts as favorites.

Displaying User Favorite Posts

Won't it be nice for users to be able to see all the posts they have marked as favorites? Sure it will be. Remember we defined a my_favorites route that will be accessible to only authenticated users, this is where users will be able to see the posts they've marked as favorites.

Let's create a UsersController that will handle this route.

php artisan make:controller UsersController

Open app/Http/Controllers/UsersController.php and add the code below to it:

// app/Http/Controllers/UsersController.php

// remember to use
use Illuminate\Support\Facades\Auth;

/**
 * Get all favorite posts by user
 *
 * @return Response
 */
public function myFavorites()
{
    $myFavorites = Auth::user()->favorites;

    return view('users.my_favorites', compact('myFavorites'));
}

The myFavorites() uses the favorites relationship we defined earlier, get all the posts that the authenticated user has marked as favorites. Then return a view along with favorites posts.

Now let's create the view. Create a new users folder within the resources/views directory and within the users folder, create a new file my_favorites.blade.php and paste the code below to it:

// resources/views/users/my_favorites.blade.php

@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-8 col-md-offset-2">
            <div class="page-header">
                <h3>My Favorites</h3>
            </div>
            @forelse ($myFavorites as $myFavorite)
                <div class="panel panel-default">
                    <div class="panel-heading">
                        {{ $myFavorite->title }}
                    </div>

                    <div class="panel-body">
                        {{ $myFavorite->body }}
                    </div>
                    @if (Auth::check())
                        <div class="panel-footer">
                            <favorite
                                :post={{ $myFavorite->id }}
                                :favorited={{ $myFavorite->favorited() ? 'true' : 'false' }}
                            ></favorite>
                        </div>
                    @endif
                </div>
            @empty
                <p>You have no favorite posts.</p>
            @endforelse
         </div>
    </div>
</div>
@endsection

The markup is similar to that of index.blade.php. As you can see, we also used the Favorite component. When viewed in the browser, you should see something similar to:

Conclusion

That's it, we are done building the Post app and seen how to allow only authenticated users mark/unmark posts as favorites without reloading the page using VueJs and Axios. I hope you find this tutorial useful. If you encounter any problems following this tutorial or have questions/suggestions, kindly drop them in the comment section below. Also, I have made a vue-favorite component based on this tutorial which can be installed through NPM.

Chimezie Enyinnaya

9 posts

Software Developer [PHP Laravel JavaScript NodeJS AdonisJS VueJS] | movie lover | run http://openlaravel.com