Build a Blog with Laravel Livewire

October 08, 2024
Written by
Kenneth Ekandem
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

When Laravel started out, it was primarily targeted at backend developers, while frontend developers used other tools/frameworks like React, Angular, etc.. 

Why was this? Laravel didn’t support essentials like DOM manipulation, dynamic interfacing, etc. These concepts could only be utilised outside of Laravel, making Livewire a game changer.

In this article, we will dive into several Livewire concepts, and consider how these concepts enable easy integration between the backend and rendering feedback on the frontend using Livewire. Then, we'll build a Livewire application, so that you get hands-on experience with them.

Prerequisites

To follow along with this tutorial, you will need the following:

  1. PHP 8.3

  2. Composer installed globally

  3. Prior experience with PHP

  4. Prior experience with Laravel

  5. Some command-line experience would be ideal, though not required

What is Laravel Livewire?

Livewire is a framework built for Laravel that fuses the backend with the frontend. It allows the manipulation of the DOM, dynamic interfaces, and how requests are handled between the controllers and views. 

Furthermore, this is all handled within Laravel, and integrates perfectly with JavaScript tools like Alpine.js, CSS frameworks like Tailwind CSS, and local plugins like Laravel Echo

Typically, a new development project will use separate tools and frameworks for building the backend and frontend. Livewire, however, incorporates both aspects into the same monolithic Laravel application. This enables easier application development.

Some good Livewire use cases

For validation

In programming, validation is essential to building an application. Livewire keeps that going by simplifying tasks such as form validation, and input type/data validation, etc, as in the examples below. 

In a Livewire resource file, for example: resources/views/livewire/blogs.php, the below code demonstrates how Livewire uses the `wire:model` method to bind the value of the input property.

<div class="mb-3">
    <label for="create-title" class="form-label">What's the title?</label>
    <input type="text" wire:model="title" wire:dirty.class="border border-success" class="form-control" id="create-title">
</div>

Then, similarly in a Livewire component file, e.g., app/Livewire/blogs.php, the title value is passed into the $rules protected array for validation. Following Laravel’s native validation, the function won’t continue to execute if the validate() method fails.

…
class Blogs extends Component {
    public $title;
    protected $rules = [
        'title' => 'required'
    ];
    public function create() {
        $this->validate($rules)
        return true;
    }
}

For more validation control on the front end, submitting can be prevented using the wire:submit.prevent method if the required validation options are not met. For example, if a required field like title is left empty.

<form wire:submit.prevent="create()">
…
<input type="text" wire:model="title" class="form-control" id="create-title" required>
</form>

For handling errors

Errors from form validation can be logged directly to the frontend using the @error method. The @error tag takes the model like title and returns the validation message; for example:

<input type="text" wire:model="title" class="form-control" id="create-title" required>
<span>@error('title') {{ $message }}  @enderrror</span>

It's component-based

Livewire makes the interaction between Laravel Blade templates and their corresponding components alongside other Laravel internal methods seamless. Usually, you’d install JQuery and Alpine.js to initiate browser events like onClick in a Blade template.

With Livewire, events that directly update the component can be triggered using methods like wire:click. The process then requests server-side code as an AJAX request and returns them to the template without reloading. 

To demonstrate, below, a button created in resources/views/livewire/counter.php utilises the wire:click to increment a count every time it is clicked.

<button wire:click="increment()">increase</button>
<h3>{{ $count }} </h3>

Its corresponding component app/Livewire/Counter.php, contains the increment() function that increments the $count variable by one, every time it is initiated.

public $count = 0;

public function increment() {
    $this->count++
}

If you’re a fan of Alpine.js, the behaviour change of $count can also be displayed in markup:

<h3 x-text="$wire.count"></h3>

The $wire() method is an Alpine.js component embedded into the Livewire template. So, it can be called and used to interact with component public variables and functions alike.

The $wire() method goes beyond just interacting with functions and variables, to initiating functions that expect parameters and act as requests. Check out the documentation for more.

For state tracking

State change comes in handy when real-time tracking of changes is important. An example of this could be displaying more content based on a click() method, for example:

<div x-data="{open:false}">
    <button @click="open = true">open</button>
    <div x-show="open" @click.outside="open = false">
        <button x-click="archive">archive</button>
        <button x-click="delete">delete</button>
    </div>
</div>

Build an example Livewire application

In this section, we will build a small blog application using Livewire, and, within it, cover further important concepts like validation, state change, and rendering components.

To start, we need to create a new Laravel project. We will call the project: laravel_livewire.

composer create-project laravel/laravel laravel_livewire
cd laravel_livewire

Next, install the Livewire package using the below command.

composer require livewire/livewire

Then, open the project in your text editor or IDE of choice. Add the Livewire style and script to resources/views/welcome.blade.php, by adding @livewireStyles just before </head> and @livewireScripts just before </body>, as depicted in the example below.

<html>
<head>
    <!-- ...existing code →
    <link rel="stylesheet" href="{{ asset('css/app.css')}}">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
    @livewireStyles
</head>
<body>
    <!-- ...existing code –>
    @livewireScripts
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

</body>
</html><html>
<head>
    <!-- ...existing code →
    <link rel="stylesheet" href="{{ asset('css/app.css')}}">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
    @livewireStyles
</head>
<body>
    <!-- ...existing code –>
    @livewireScripts
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>

</body>
</html>

I've used Bootstrap and custom CSS for customising the views. The linked asset (css/app.css) can be found in the GitHub repository for this article. Download it to a new directory, named css, in the public directory.

Create the blog controller

Next, we have to create our Livewire Blogs controller, by running the following command in the project's top-level directory.

php artisan make:livewire blogs

This will create two files, a controller file in app/Livewire/Blogs.php and a Livewire view file in resources/views/livewire/blogs.blade.php.

Now, in the welcome.blade.php file, include the new Livewire views by updating the body tag to match the following.

<body>
    <livewire:blogs />
    @livewireScripts
</body>

Insert blog data

To store the blog data, we will first generate a migration file to create a blog table. To do that, run the following commands in the terminal.

php artisan make:migration create_blogs_table
php artisan make:model Blog

These will create a new migration file for a "blogs" table, and create a Laravel model to interact with the table, when the migration command is run later in this article.

Next, run the commands below to generate a factory which will be used to insert a couple of records to display in our Livewire view later in the article, and a seeder to seed fake records in the "blogs" table.

php artisan make:factory BlogFactory
php artisan make:seeder BlogSeeder

Then, edit the "blogs" table's migration (it's the file ending with blogs_table.php in the database/migrations directory) to include the required columns, by updating the up() function to match the code below.

public function up(): void
{
    Schema::create('blogs', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->string('description');
        $table->integer('likes');
        $table->integer('dislikes');
        $table->timestamps();
    });
}

These changes need to be reflected in the database model class. So, in app/Models/Blog.php add a primary key and fillable parameters for the "blogs" table, by updating the file to match the code below.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Blog extends Model
{
    use HasFactory;

    protected $primaryKey = 'id';
    protected $table = 'blogs';

    protected $fillable = [
        'title',
        'description',
        'likes',
        'dislikes'
    ];
}

Then, (if required) update the database variables in .env to use SQLite as the database backend.

DB_CONNECTION=sqlite

After that, run the migration using the command below.

php artisan migrate

Next, update the definition() function, in database/factories/BlogFactory.php, to match the code below, so that it generates random fake records to fill the "blogs" table.

public function definition(): array
{
    return [
        'title' => $this->faker->sentence(3),
        'description' => $this->faker->sentence(20),
        'likes' => 0,
         'dislikes' => 0
    ];
}

Then, in the seeder file, database/seeders/BlogSeeder.php, seed 10 records with the BlogFactory by updating the run() function to match the implementation below.

public function run(): void
{
    \App\Models\Blog::factory(10)->create();
}

Then in database\seeders\DatabaseSeeder.php, update the run() function to include the blogs table seeder.

public function run(): void
{
     User::factory()->create([
         'name' => 'Test User',
         'email' => 'test@example.com',
     ]);

    $this->call([
        BlogSeeder::class,
    ]);
}

With this all in place, run the Artisan console's seed command to add the information to the database.

php artisan db:seed

Show blog entries

Earlier in this article, we included the Livewire blog template in resources/views/welcome.blade.php using the <livewire:blogs /> tag. 

In this section, we will fetch all the entries from the "blogs" table to give an example of how Livewire filters data and displays records, and also what our blog will look like with data.

To do that, in app/Livewire/Blogs.php update the render() function to include the code below.

<?php

namespace App\Livewire;

use App\Models\Blog;
use Livewire\Component;
use Livewire\WithPagination;

class Blogs extends Component
{
    use WithPagination;

    public function render()
    {
        return view('livewire.blogs', [
            'blogs' => Blog::orderBy('created_at', 'desc')->simplePaginate(10),
            'count' => Blog::count()
        ]);
    }
}

The code above performs a simple fetch using Laravel pagination in the Livewire component. I have also taken the liberty of adding a call to count() to track the count of the blog entries retrieved.

Then, update the template in resources/views/livewire/blogs.blade.php to loop through the retrieved blog entries using the @foreach tag, and add blogs->links() to display pagination templates for the blogs.

<div class="col-12">
    <div class="row justify-content-center">
        <div class="col-7 centered-div pb-4">
           <div class="row mt-2 p-2">
                <div class="col-6">
                    <h1 class="logo pl-5"><i>Lucy ☁️</i></h1>
                </div>
                <div class="col-6  d-flex justify-content-end  align-content-end pt-3">
                </div>

                <div class="col-12 mt-5 justify-content-center blogs">
                    <div class="row">
                        <div class="col-6">
                            <h3 class="mt-3">Total Blogs <span class="badge badge-dark" style="background: black">{{ $count }}</span></h3>
                        </div>
                        <div class="col-6 d-flex justify-content-end">
                            <button class="btn btn-sm p-background" data-bs-toggle="offcanvas" href="#offcanvasExample" role="button" aria-controls="offcanvasExample">Create Blog</button>
                        </div>
                    </div>
                    <div class="row mt-4">
                        @foreach ($blogs as $blog)
                            <div class="col-sm-6">
                                <div class="card blog-card mb-5">
                                    <div class="card-body">
                                        <h5 class="card-title">{{ $blog->title}} - <small>{{ date('d-m-Y', strtotime($blog->created_at)) }}</small></h5>
                                        <p class="card-text">{{ $blog->description }}</p>
                                        <div class="row">
                                            <div class="col-2 blog-actions">
                                                <ion-icon name="heart"></ion-icon> <span>{{ $blog->likes }}</span>
                                            </div>
                                            <div class="col-2 blog-actions">
                                                <ion-icon name="heart-dislike-outline"></ion-icon> <span>{{ $blog->dislikes }}</span>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        @endforeach
                        <div class="d-flex">
                            {!! $blogs->links() !!}
                        </div>
                    </div>
                </div>
           </div>
        </div>
    </div>
</div>

Templating in Livewire uses syntax from Laravel and hence uses the @foreach array method to filter and recognize Laravel pagination methods; an example being {!! $blogs→link() !!}.

Naturally, a blog would incorporate a search option to fetch similar results to the search query. With methods like wire:mode.live, search queries are sent in real-time and responses are automatically injected into the template.

To do that, in our resources/views/livewire/blogs.php template, add our search tags by adding the code below just below the <h1> tag.

<div class="col-6  d-flex justify-content-end  align-content-end pt-3">
    <div class="row">
        <div class="col-12 p-0 m-0">
            <input wire:model.live="search" class="form-control" type="text" placeholder="Search" aria-label="Search">
        </div>
    </div>
</div>

The search tag uses the wire:model.live method to listen and send live updates to the component model search. So, every time text is input, the "blogs" table is searched, filtered by the input text, and the records returned update the blogs template.

Next, update the Blogs component, app/Livewire/Blogs.php, to handle search, by updating it to match the code below.

<?php

namespace App\Livewire;

use App\Models\Blog;
use Livewire\Component;
use Livewire\WithPagination;

class Blogs extends Component
{
    use WithPagination;

    public $search;
    protected $queryString = ["search"];

    public function render()
    {
        return view('livewire.blogs', 
            [
                'blogs' => Blog::search('title', $this->search)
                                    ->orderBy('created_at', 'DESC')
                                    ->simplePaginate(10),
                'count' => Blog::count()
            ]
        );
    }
}

In the component above:

  • A public $search variable has been added

  • A protected $queryString command has been added to keep the query parameter active even after refresh

  • The render() function's been updated to use the search macro to filter the blogs title column records using the value of search

In the render() function, Blog::search() search is a macro written into the Laravel application. Add it in app/Providers/AppServiceProvider.php by updating the boot() method to match the definition below.

public function boot(): void
{
    \Illuminate\Database\Eloquent\Builder::macro('search', function ($field,  $string) {
        return $string ? $this->where($field, 'like', '%'.$string.'%') : $this;
    });
}

Here’s what it looks like.

Dashboard with blog stats, four blog previews, and a button to create new blog

Create a blog post

Now, let’s create a new blog post to understand, more insightfully, how Livewire handles validation and requests.

Create a new Livewire component and template with the following commands.

php artisan make:livewire create

Then, include some HTML in the newly created resources/views/livewire/create.blade.php template file.

<div class="offcanvas offcanvas-start" tabindex="-1" id="offcanvasExample" aria-labelledby="offcanvasExampleLabel">
  <div class="offcanvas-header">
    <h3 class="offcanvas-title p-color" id="offcanvasExampleLabel">Share your thoughts...</h3>
    <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
  </div>
  <div class="offcanvas-body">
    <form wire:submit.prevent="create">
        <div class="mb-3">
            <label for="create-title" class="form-label">What's the title?</label>
            <input type="text" wire:model="title" wire:dirty.class="border border-success" class="form-control" id="create-title">
            @error('title') 
            <div class="alert alert-primary" role="alert">
              <span class="error">{{ $message }}</span> 
            </div>
            @enderror
            {{-- @error('title') <span class="error">{{ $message }}</span> @enderror --}}
        </div>
        <div class="mb-3">
            <label for="create-description" class="form-label">Share thoughts</label>
            <textarea name="" id="" wire:model="description" cols="10" rows="5" id="create-description" class="form-control form-textarea"></textarea>
            @error('description') 
              <div class="alert alert-primary" role="alert">
                <span class="error">{{ $message }}</span> 
              </div>
            @enderror
        </div>
        <div class="d-grid">
            <button type="submit" class="btn p-background btn-block">Post <span wire:loading></span></button>
        </div>
    </form>
  </div>
</div>

Then in the resources/views/livewire/blogs.blade.php update the create blog button to open the canvas.

…
<div class="col-6 d-flex justify-content-end">
<button class="btn btn-sm p-background" data-bs-toggle="offcanvas" href="#offcanvasExample" role="button" aria-controls="offcanvasExample">Create Blog</button>
</div>
<livewire:create></livewire:create>
  • The template input for the title and description fields also includes @error tags. This will log a validation error message to the off-canvas Livewire component.

  • In the form tag, the Livewire wire:submit.prevent="create" method is set in the form to prevent submission if all the required parameters are not met. For example, if the user initiates the submission without filling out the description field. 

Next, update the app/Livewire/Create.php component to include validation and create a new blog.

<?php
namespace App\Livewire;

use App\Models\Blog;
use Livewire\Component;

class Create extends Component
{
    public $title;
    public $description;

    protected $rules = [
        'title' => 'required',
        'description' => 'required'
    ];

    public function render()
    {
        return view('livewire.create');
    }

    public function create() {
        $this->validate($this->rules);
        $blog = Blog::create([
            'title' => $this->title,
            'description' => $this->description,
            'likes' => 0,
            'dislikes' => 0
        ]);
        return redirect('/'); 
    }
}

In the above code, we added validation to check the $rules, just like is used in Laravel.

Additions

Remember how our blog entries have likes and dislikes. We will make them interactive by tracking clicks and updating their count. To do that, update the like and dislike tags in the blog template (resources/views/livewire/blogs.blade.php) to include the code below.

<div class="col-2 blog-actions">
    @if($blog->likes > 0)
        <span><i class="fa-solid fa-heart"></i><span>{{ $blog->likes }}</span></span>
    @else
        <span type="button" wire:click="like({{ $blog->id}})"><i class="fa-regular fa-heart"></i><span>{{ $blog->likes }}</span></span>
    @endif
</div>
<div class="col-2 blog-actions">
    <span><i class="fa-solid fa-thumbs-down" type="button" wire:click="dislike({{ $blog->id}})"></i><span>{{ $blog->dislikes }}</span></span>
</div>

Then, in app/Livewire/Blogs.php, include the functions to update the count for both likes and dislikes on a single blog, by adding the like() and dislike() functions below.

public function like($id) {
    $blog = Blog::where('id', $id)->first();
    $blog->update([
        'like' => $blog->likes++
    ]);
    $blog->save();
    return $blog;
}

public function dislike($id) {
    $blog = Blog::where('id', $id)->first();
    $blog->update([
        'dislike' => $blog->dislikes++
    ]);
    $blog->save();
    return $blog;
}

After fetching the blog from the given $id, the functions will update the like count, and then return the blog. This will automatically update the blog in Livewire.

Now, go ahead and launch your application by running the following command.

php artisan serve

Testing

Now that the application has been set, we can run a test to create a new blog, search for a blog, and drop a like or dislike reaction to the blog.

Create Blog

To create, go to http://localhost:8000 on the web browser and click on the “create blog” button, like in the example below, to open the Create Blog canvas.

Popup for user to share thoughts on content about Livewire forms on a blog website

Add a title and description to the new blog post and click “post”. The newly created blog will be rendered on the resources/views/livewire/blogs.blade.php file. Also, notice in the next image that the blog count also increments to track the addition.

Interface displaying a blog dashboard with different posts, interaction buttons and options to create a new post.

Search Blog

Next, search for a random title, in this case, the search will be “content”. It’ll filter the database for related blog titles that contain “content” as a word and render that result to the page.

Screenshot of a blog dashboard showing total blog count, content description, and create blog button.

React to blog

Next, go ahead and click on a reaction for a random blog.

Interface displaying a blog dashboard with different posts, interaction buttons and options to create a new post.

Conclusion

In this article, we reviewed Livewire and its fundamentals, such as rendering components, change in state, validation, etc., and several use cases. We also implemented a blog website to explain the different concepts Livewire offers. 

With this in mind, LiveWire will be a great package to adopt when building the next blog with a state change requirement while maintaining safe forms and adding great filtering.

Kenneth Ekandem is a software engineer from Nigeria currently in the blockchain space, but interested in learning everything computer science has to offer. He'd love to go to space one day and own his own vlogging channel to teach the next generation of programmers. You can reach out to him on Twitter, LinkedIn, and GitHub.