Laravel Livewire CRUD without Controllers

Laravel Livewire CRUD without Controllers

ยท

7 min read

In this article, we are going to create a basic CRUD using Laravel 8, Livewire 2.x.

Table of contents

  1. Create new Laravel project
  2. Set up database credentials
  3. Install Laravel UI
  4. Post model, migration, factory and seeder
  5. Install Laravel Livewire
  6. Create livewire components
  7. Add livewire routes
  8. Set up Post Index component
  9. Create Post Component
  10. Edit Post Component
  11. Delete Post
  12. Github Repo

1. Create new Laravel project

Let's create a new project called Laravel-Livewire-CRUD with the following command:

composer create-project laravel/laravel Laravel-Livewire-CRUD

Or you can use the global installer

laravel new Laravel-Livewire-CRUD

2. Set up database credentials.

In the .env file, modify the next lines with your credentials

database_credentials.png

3. Install Laravel UI

In this tutorial, I'm going to use Laravel UI but you can use Laravel Jetstream, Breeze, or Fortify.

Use this command to install Laravel UI

composer require laravel/ui

After that, we need to install the frontend scaffold with registration using this command

php artisan ui bootstrap --auth

If everything is correct, execute

npm install && npm run dev

The next step is migrate our tables with php artisan migrate command, when you complete this step, you should be able to see the register view.

register.png

Cool. Now we can log in.

4. Post model, migration, factory and seeder

We have to create a Post model with its migration, factory, and seeder

php artisan make:model Post -mfs

Next, we have to search posts migration inside the database folder

migrations.png

And inside the migration add the following lines

  public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('post_text');
            $table->timestamps();
        });
    }

Now, in our model add the next lines

 protected $fillable = [
        'title',
        'post_text'
    ];

That's it, our model and migration are ready, the next step is go to app/database/factories/PostFactory.php file and inside the definition function add

return [
            'title' => $this->faker->words(3, true),
            'post_text' => $this->faker->paragraph()
        ];

Alright almost done here, now we go to app/database/seeders/PostSeeder.php file, add this

public function run()
    {
        Post::factory()->count(5)->create();
    }

Finally, in app/database/seeders/DatabaseSeeder.php, add

public function run()
    {
        $this->call([
            PostSeeder::class,
        ]);
    }

Great!!! everything is ready, now execute php artisan migrate --seed. We're done here. The next step is Livewire integration.

5. Install Laravel Livewire

composer require livewire/livewire

After successfully installing, you have to search resources/views/layouts/app.blade.php file and add @livewireStyles, @livewireScripts like the image.

livewire_layout.png

6. Create livewire components

Let's create 3 components: Post Index, Post Create, Post Edit.

php artisan make:livewire posts.index
php artisan make:livewire posts.create
php artisan make:livewire posts.edit

Each component generates two files in our project

livewire_component.png

7. Add livewire routes

In web.php file, let's define the routes of the project.

Route::get('/posts/index', App\Http\Livewire\Posts\Index::class)->name('posts.index');
Route::get('/posts/create', App\Http\Livewire\Posts\Create::class)->name('posts.create');
Route::get('/posts/edit/{post}', App\Http\Livewire\Posts\Edit::class)->name('posts.edit');

8. Set up Post Index component

Before configure the Post Index component we need to add Post navigation in app/resources/views/layouts/app.blade.php like the image below.

<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
            <div class="container">
                <a class="navbar-brand" href="{{ url('/') }}">
                    {{ config('app.name', 'Laravel') }}
                </a>
                <a class="navbar-brand" href="{{ route('posts.index') }}">
                    Post
                </a>
            </div>
</nav>

You should see this menu bar

menu.png

Now, we can continue. Remember Livewire generates two files, first let's go to app/Http/Livewire/Posts/Index.php and modify like the image below.

<?php

namespace App\Http\Livewire\Posts;

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

class Index extends Component
{
    use WithPagination;

    protected $paginationTheme = 'bootstrap';

    protected $listeners = ['deletePost'];

    public function deletePost($id)
    {
        Post::find($id)->delete();

        session()->flash('message', 'Post successfully deleted.');
    }

    public function render()
    {
        $posts = Post::paginate(5);

        return view('livewire.posts.index', ['posts' => $posts])
            ->extends('layouts.app')
            ->section('content');
    }
}

Let me explain some things here:

  • Laravel Livewire can handle pagination through withPagination trait.

  • Livewire uses TailwindCSS as the default style of pagination, in this case, we want to use Bootstrap and we have to add $paginationTheme.

  • In the render method is necessary to add ->extends and ->section to specify where the component will render. if you want more info you can check Full-Page Components Livewire Official Docs.

  • We have a listener deletePost, this listener becomes active when the user clicks the button to delete one Post, this action automatically sends an event with the Id we want to delete. You can understand more with the posts/index.blade.php view down below.

After that, let's go to our view app/resources/views/livewire/posts/index.blade.php and create something like that.

<div>
    <div class="container">
      @if (session()->has('message'))
        <div class="alert alert-success">
            {{ session('message') }}
            <button type="button" class="close" data-dismiss="alert">x</button>
        </div>
        @endif
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header">
                        <div class="float-right">
                            <a href="{{route('posts.create')}}" class="btn btn-primary"> Create new post </a>
                        </div>
                    </div>
                    <div class="card-body">
                     <table class="table table-bordered table-striped">
                            <thead>
                                <tr>
                                    <th scope="col">Title</th>
                                    <th scope="col">Post text</th>
                                    <th scope="col">Action</th>
                                </tr>
                            </thead>
                            <tbody>
                                @forelse ($posts as $post)
                                <tr>
                                    <td>{{$post->title}}</td>
                                    <td>{{$post->post_text}}</td>
                                    <td>
                                        <a href="{{route('posts.edit', $post->id)}}" class="btn btn-sm btn-success"> Edit </a>
                                        <button type="button" onclick="deletePost({{$post->id}})" class="btn btn-sm btn-danger">Delete</button>
                                    </td>
                                </tr>
                                @empty
                                <tr>
                                    <td colspan="3">No posts found</td>
                                </tr>
                                @endforelse
                            </tbody>
                        </table>
                        {{$posts->links()}}
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script>
     function deletePost(id) {
        if (confirm("Are you sure to delete this record?"))
            Livewire.emit('deletePost', id);
     }
    </script>
</div>

Note: Livewire.emit fire an event called deletePost that we receive in Posts/Index.php. If you want to learn more about Livewire Events visit Livewire Events

Great!!!, you should see something like that.

post_index.png

9. Create Post Component

The next step is to search app/Http/Livewire/Posts/Create.php file and modify like the image below.

<?php

namespace App\Http\Livewire\Posts;

use App\Models\Post;
use Livewire\Component;

class Create extends Component
{
    public $title, $post_text;

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

    public function store()
    {
        Post::create($this->validate());

        session()->flash('message', 'Post successfully created.');

        return redirect()->route('posts.index');
    }

    public function render()
    {
        return view('livewire.posts.create')
            ->extends('layouts.app')
            ->section('content');
    }
}

Right now we want to modify the app/resources/views/livewire/posts/create.blade.php view with our form to create a new post.

<div>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header">Create new post</div>
                    <div class="card-body">
                        <form wire:submit.prevent="store">
                            <div class="form-group">
                                <label for="title">Title</label>
                                <input type="text" wire:model="title" class="form-control" id="title" placeholder="Enter a title">
                                @error('title') <span class="text-danger">{{ $message }}</span> @enderror
                            </div>
                            <div class="form-group">
                                <label for="post_text">Post text</label>
                                <textarea cols="80" rows="6" class="form-control" wire:model="post_text" name="post_text"></textarea>
                                @error('post_text') <span class="text-danger">{{ $message }}</span> @enderror
                            </div>
                            <button type="submit" class="btn btn-primary">Submit</button>
                            <a href="{{route('posts.index')}}" class="btn btn-danger">Back</a>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Note: every time we want to create a new post wire:submit.prevent="store" execute the method store in our Posts/Create.php file

10. Edit Post Component

It's almost the same as that Create component but in the Edit.php file, we need to edit one Post in particular, that's why we use Route Model Binding through public Post $post variable and mount function to initialize the values of the Post.

<?php

namespace App\Http\Livewire\Posts;

use App\Models\Post;
use Livewire\Component;

class Edit extends Component
{
    public Post $post;

    public $title;
    public $post_text;

    public function mount()
    {
        $this->title = $this->post->title;
        $this->post_text = $this->post->post_text;
    }

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

    public function update()
    {

        $this->post->update($this->validate());

        session()->flash('message', 'Post successfully updated.');

        return redirect()->route('posts.index');
    }

    public function render()
    {
        return view('livewire.posts.edit')
            ->extends('layouts.app')
            ->section('content');
    }
}

Next step is to search the app/resources/views/livewire/posts/edit.blade.php view and add this code.

<div>
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-12">
                <div class="card">
                    <div class="card-header">Update post</div>
                    <div class="card-body">
                        <form wire:submit.prevent="update">
                            <div class="form-group">
                                <label for="title">Title</label>
                                <input type="text" wire:model="title" class="form-control" id="title" placeholder="Enter a title">
                                @error('title') <span class="text-danger">{{ $message }}</span> @enderror
                            </div>
                            <div class="form-group">
                                <label for="post_text">Post text</label>
                                <textarea cols="80" rows="6" class="form-control" wire:model="post_text" name="post_text">
                                </textarea>
                                @error('post_text') <span class="text-danger">{{ $message }}</span> @enderror
                            </div>
                            <button type="submit" class="btn btn-primary">Submit</button>
                            <a href="{{route('posts.index')}}" class="btn btn-danger">Back</a>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

11. Delete post

To be honest, we can delete a post when setup the Posts/Index.php component but let's verify if everything works fine. When click the delete button should show this window.

delete_post.png

After that, you should see.

delete_success.png

12. Github Repo.

This is the link to the repo. Laravel Livewire Crud Repo

Happy coding โ˜•. Thanks for reading ๐Ÿ™.

ย