Hello and welcome to this episode of the tutorial series. In the previous episode you learnt how to create factories and seed our database with sample data. This episode will begin with building the UI part of our blog using Laravel Livewire components.
At the end of the tutorial, you should be able to:
- Describe what a Livewire component is.
- Create and modify Livewire components to suit your needs.
- Use Tailwind CSS utility classes to practically layout elements in your Blade view files.
As usual, fasten up your seat belt and let's dive in. :)
Note: There is a starter project up to this episode which can be accessed on Github here
Introduction
Part Two Recap
Remember in part two of this tutorial series we configured the public facing routes to return the default Laravel Jetstream welcome page. If you also remember, there were exactly two routes for the public facing pages of our blog - the home and the category pages. The homepage will be a listing of our blog posts while the category page will list all blog posts of a particular category. You can always read part two of this tutorial series here if you missed that part.
You're going to work on those two pages and replace the welcome page with a more interesting listing of your blog posts for this episode, albeit with introduction to some Tailwind CSS utilities. OK. Read on.
Introduction to Livewire Components
Livewire components are reusable pieces of code that you can define once and use in different parts of your application. They are just like Laravel components but comes with the power of both Laravel and Livewire. By default, a Livewire component is created with a corresponding view file. Livewire component classes are also placed in the app/Http/Livewire directory while their corresponding views are placed in the resources/views/livewire directory.
Creating Livewire Components
Livewire components are created by running the make:livewire
or livewire:make
Artisan commands in your terminal:
php artisan make:livewire PostItem
// or ...
php artisan livewire:make PostItem
This creates the most basic Livewire component, which extends the base Livewire Component
class and contains a render
method - used for rendering views and inline text:
<?php // tall-blog/app/Http/Livewire/PostItem.php
namespace App\\Http\\Livewire;
use Livewire\\Component;
class PostItem extends Component {
public function render() {
return view('livewire.post-item');
}
}
and the corresponding view:
{{\-- tall-blog/resources/views/livewire/post-item.blade.php --}}
<div>
{{\-- If you look to others for fulfillment, you will never truly be fulfilled. --}}
</div>
Adding the --inline
option tells Livewire that your component is an inline component and won't be returning any view in its render
method. So this:
php artisan make:livewire PostItem --inline
yields something like this:
...
public function render() {
return <<<'blade'
<div></div>
blade;
}
...
It is important to understand that public
properties in a Livewire component are readily available in the component's view file, so you don't need to pass it through the view
method like you're used to do if you've worked with pure Laravel controllers:
// This:
class PostItem extends Component {
public $post;
public function render() {
return view('livewire.post-item');
}
}
// is the same as this:
class PostItem extends Component {
public $post;
public function render() {
return view('livewire.post-item', ['post' => $post]);
}
}
Rendering Livewire Components
Livewire components are meant to be reusable. As a result, you can use them anywhere you would a Laravel component. Rendering a component can be done by either using the <livewire:component-name />
tag syntax or by using the @livewire('component-name')
blade directive syntax:
<div>
<livewire:post-item />
</div>
{{-- Or --}}
<div>
@livewire('post-item')
</div>
Passing Parameters to Components
You can pass parameters to a component by specifying those parameters like so:
<livewire:post-item :post="$post" />
{{-- or --}}
@livewire('post-item', ['post' => $post])
Now the $post
variable will be available in the PostItem component and view.
Accessing Route Parameters
In a situation whereby you need to access route parameters like you would in a traditional Laravel controller, Livewire allows you to do that in the mount
method:
class MyComponent extends Component {
public $userId;
public function mount($userId) {
$this->userId = $userId;
}
public function render() {
return view('livewire.my-component');
}
}
This feature makes it so powerful for Livewire components to mimic the behavior of Laravel controllers and also makes it easy to make full page components.
Creating Our Blog's Components
Now that you have a solid understanding of Livewire components, let's move on to work on our blog. If you haven't yet done so from above, create your first Livewire component for the project by running this command in your terminal:
php artisan make:livewire PostItem
This component represents one post item from a list in the home and category pages. Open the component class in app/Http/Livewire/PostItem.php and add a public $post
property to the beginning. The component class should look similar to this:
<?php // tall-blog/app/Http/Livewire/PostItem.php
namespace App\Http\Livewire;
use Livewire\Component;
class PostItem extends Component {
public $post;
public function render() {
return view('livewire.post-item');
}
}
As pointed out above, the $post
property will now be readily available in the view file for use since it is declared public
. Open the corresponding blade view file in resources/views/livewire/post-item.blade.php. Make sure it contains the following:
{{-- tall-blog/resources/views/livewire/post-item.blade.php --}}
<article class="flex flex-col mb-2 rounded-md shadow-md md:mb-0">
<a href="{{ route('post-detail', ['slug' => $post->slug]) }}">
<img src='{{ asset("storage/posts/$post->featured_image") }}'
alt="{{ $post->title }}" class="w-full h-56 rounded-t-md">
</a>
<div class="p-3">
<h3 class="text-lg font-semibold text-gray-900">
<a href="{{ route('post-detail', ['slug' => $post->slug]) }}">
{{ $post->title }}
</a>
</h3>
<p class="text-gray-800">
<a href="{{ route('post-detail', ['slug' => $post->slug]) }}">
{{ $post->excerpt }}
</a>
</p>
<div class="flex flex-row justify-between mt-2">
<a href="{{ route('category', ['category' => $post->category]) }}"
class="px-2 text-sm text-indigo-900 bg-indigo-100 rounded">
{{ $post->category }}
</a>
<small>
{{ $post->published_date }}
</small>
</div>
</div>
</article>
First of all, you can see that we're making use of the $post
variable in this file, which contains a blog post entry. We're utilizing Tailwind CSS class utilities to style the root <article>
element. In this case, we have laid its contents in a Flexbox with a flex-direction
of col
- meaning we want to lay the contents from the top to bottom. mb-2
is a utility class for 8-pixels margin-bottom value with rounded-md
and shadow-md
representing medium rounded and medium shadowed shapes respectively.
Next is the anchor element (a
tag). We're referencing a named Route to route to the post detail page(using the route
helper function) which uses the post's slug to construct the link. Remember we didn't create the post detail route so you would need to add it just below the homepage route:
Route::get('/', function () {
return view('welcome');
})->name('home');
// Add below route
Route::get('{slug}', function ($slug) {
return view('welcome');
})->name('post-detail');
Next in the tree is the image, whose width we have made full (100%) with w-full
and whose height we have decided will be h-56
(equivalent to 224px) with its top rounded. The post image's src
attribute points to an image in the posts folder which is in the storage
symlink we created earlier. asset
is also one of the helper functions that allows you to display assets(css, images, js) files in your blade views. The file itself is uploaded in the posts folder while its name is saved into the database.
The next block contains the title, excerpt, category and published date all wrapped in a nicely padded(with p-3
) div
element. Both the title and excerpt use a different shade of gray text color from Tailwind CSS's color utilities. The last thing worth noting is the fact that the category and published date of our post are also wrapped in a Flexbox div
container which runs from left to right (row
) with a justify-between
utility which ensures that these two elements are placed at the extreme ends of both left and right of the containing div
element.
The post item component is now done. Let's create another component that will list all the posts on the homepage. This component will fetch all the posts from the database and display each in the post item component we created above. Enter the command to create the show-posts component:
php artisan make:livewire ShowPosts
Open the created component at app/Http/Livewire/ShowPosts.php and make sure its contents is equal to this:
<?php // tall-blog/app/Http/Livewire/ShowPosts.php
namespace App\Http\Livewire;
use App\Models\Post;
use Livewire\Component;
class ShowPosts extends Component {
public $posts;
public function mount() {
$this->posts = Post::where('is_published', true)->get();
}
public function render() {
return view('livewire.show-posts')->layout("layouts/guest");
}
}
The $posts
property holds all our posts from the database, which we fetch when this component is mounted (this is why the query is put in the mount
method). We're only interested in posts that are published. We have also specified the layout to use (i.e guest layout file). By default, Livewire uses the resources/views/layouts/app.blade.php layout file. However, because Jetstream adds authentication and other checks to that layout, using the app layout file without those checks in place will lead to errors, that's the reason we're using the resources/views/layouts/guest.blade.php layout file.
Now open the corresponding resources/views/livewire/show-posts.bladed.php blade view and enter the following in it:
<div class="gap-4 m-2 md:grid md:grid-cols-2 lg:grid-cols-4">
@foreach ($posts as $post)
@livewire('post-item', ['post' => $post], key($post->id))
@endforeach
</div>
This one is simpler than you might have guessed. We're looping through the $posts
collection and rendering each using the PostItem
component we created earlier, passing the $post
to its post
parameter and giving its key the post id. The key is used to uniquely differentiate the current post item from the others and is very necessary when you, for example, want to toggle a post's visibility.
On the Tailwind part, we're displaying the posts in
- a block container on small screens,
- a grid container with two columns in medium screen devices and
- a grid container with four columns in large+ screen devices
All these with a 16-px gap between each item. Of course, and an 8-pixel margin.
Now change the homepage route to the following:
...
Route::get('/', App\Http\Livewire\ShowPosts::class)->name('home');
...
Hook up your terminal, run php artisan serve
and open your browser to http://127.0.0.1:8000 and you should see posts in the homepage:
Now create the last Livewire component, which will display all posts from a particular category, by running this Artisan command:
php artisan make:livewire CategoryPosts --inline
We have made the CategoryPosts
component inline because it is going to reuse the show-posts component view file. Since there is nothing different between the two except filtering from the database, it is simpler using this approach.
Modify the CategoryPosts
component to match the following:
<?php
// tall-blog/app/Http/Livewire/CategoryPosts.php
namespace App\Http\Livewire;
use App\Models\Post;
use Livewire\Component;
class CategoryPosts extends Component {
public $posts;
public function mount($category) {
$this->posts = Post::where('category', $category)
->where('is_published', true)
->get();
}
public function render() {
return view('livewire.show-posts')
->layout("layouts/guest");
}
}
This is almost the same as the ShowPosts
component except that we've changed the query to fetch based on the given category query string passed to the mount
method. Change the category route to the following:
...
Route::get('categories/{category}', CategoryPosts::class)->name('category');
...
Start the built-in PHP server if it's not running:
php artisan serve
Now go back to your homepage and click on any category. It should send you to the category page displaying posts in the same category:
This brings us to the end of this episode. I hope you really enjoyed the tutorial and I'd love to see you reading the next episode. We post updates on both our Twitter and Facebook pages. If you don't mind, you can follow any of our social media pages to get notified when a new post is published.
Twitter: https://twitter.com/bandughana
Facebook: https://www.facebook.com/bandughana
Thank you and see you for the next post.
Top comments (5)
Dear Alhassan:
The tutorial is wonderful. However, I got this error when follow the part5 instructions.
Missing required parameter for [Route: post-detail] [URI: {slug}] [Missing parameter: slug]
I appreciate you can give me hits. Thanks.
@johnjehunglin sorry for the late reply. I'm very busy recently and actually never thought I'd receive notifications again.
But coming to your issue, did you pass on the
slug
for the detail route as required below:Route::get('{slug}', function ($slug) {
return view('welcome');
})->name('post-detail');
There may be one of two problems: either the slug column is missing from your posts table or the slug wasn't passed. If you read the tutorial series to the end, you'd see that I later modified the posts table to accommodate the
slug
column.Also, you can generate an slug in the Livewire component using
Illuminate\Support\Str
Hi Alhassan,
But how can I create the database ? Is there an sql script ?
Thank you.
How did you install your database server? If you installed it through a WAMP/LAMP/MAMP or XAMPP stack, then you can start both your database and web servers and create your database through phpMyAdmin.
Else, you can create it with the following SQL statement:
CREATE DATABASE "tall-blog";
Note that this assumes you're accessing your database through a terminal client or anywhere you can run SQL commands.