Nowadays, frontend and backend interaction is necessary to realize a modern web application. The standard for page display is to dynamically display page content without reloading the page. Building pages in a modern application requires serious frontend skills, meaning the person must be a Full-Stack developer.
To make the life of backend developers easier, Livewire was developed. In this article, we will take a look at the library itself and how we can implement dynamic pages in an application without touching Javascript.
Under the hood, Livewire still uses Alpine.js, but for developers, this may not be a big deal, as you can get by without writing any Javascript code at all.
Let's get started.
After installing the Liveware package, we need to publish its configuration file using the artisan livewire:publish
command. The configuration file has the option to set the application URLs, the midware that applies to Livewire component routers, and the main layout of our application.
The default layout is layout.app. It is this layout that we need to modify and add CSS and JS connectivity using Livewire directives.
<html>
<head>
@livewireStyles
</head>
<body>
@livewireScripts
</body>
</html>
We are now ready to implement the new web interface with Laravel Livewire
After configuring the application, we need to create the first component. To do this, we use the command php artisan make:livewire ShowProfile
When created, two files are created in the project -
app/Http/Livewire/ShowProfile.php
resources/views/livewire/show-profile.blade.php
To connect the component, we need to use one of the two options - <livewire:show-profile /> or use the blade syntax: @livewire('show-profile')
. All further code will use the blade syntax, but you can do all of the above using custom HTML tags.
It's worth noting that when grouping component files into directories, you have the option of connecting nested components by specifying a point, e.g., like this: @livewire('profile.show')
.
A large number of components have a dynamic part that must be filled in on the server side of the application. In order to plug parameters into the component, we can simply add this parameter as a property of the component class, for example like this:
namespace App\Livewire;
use Livewire\Component;
class ShowProfile extends Component
{
public $name = 'Simon';
public function render()
{
return view('livewire.profile.show');
}
}
Quite often, when building an application page, we need to build components into a tree, similar to a DOM tree. To optimize the number of database queries and to get the overall state, we can pass the values of the component properties when connecting the component itself: @livewire('show-profile', ['name' => $user->name])
In this case, the component property will be equated to the value in this array before the component itself is rendered.
For most backend developers, working with actions is the most important part of client code. To get the data, we need to make a form, just like in a normal HTML, but we will add some "magic" attributes so that Livewire can pass us the values from the form inputs directly to the properties of the component class.
<input type="text" wire:model="name">
<input type="text" wire:model="username">
<button wire:click=”create”>Create</button>
namespace App\Livewire;
use Livewire\Component;
use App\Models\Profile;
use App\Models\User;
class CreateProfile extends Component
{
public User $user;
public $name;
public $username;
public function mount()
{
$this->user = auth()->user();
}
public function create()
{
Profile::create([
'name' => $this->name,
'username' => $this->username,
'user_id' => $this->user->id,
]);
return redirect()->to('/profile');
}
public function render()
{
return view('livewire.profile.create’);
}
}
When the Create button is clicked, the data will be synchronized between the component property and the data input. Therefore, we can simply retrieve the new passed values through the properties themselves. As you may have noticed, the component also has a User passed to it via the mount() method.
This allows us to make it possible to specify the user in the component on the backend without having to pass unnecessary data back and forth to the client side.
Every time we execute a method in a component, we would like to make sure that the properties in the component can be trusted, but we can't do this without additional checks when the method is called.
To validate the data, we can use the validate method, just like we do with the Request in the Laravel controller. It is worth noting that for validating the methods of the component, the Rules are the same.
public function create()
{
$validated = $this->validate([
'name' => 'required|min:3',
'username' => 'required|unique:profiles:username|min:3',
]);
Profile::create([
'name' => $this->name,
'username' => $this->username,
'user_id' => $this->user->id,
]);
return redirect()->to('/profile');
}
If you want to use methods of the Rule class, for example, to pass the current username to be excluded from the check to the rule unique, you can use the rules method in which you need to define all checks.
public function rules()
{
return [
'title' => Rule::unique('profiles', 'username')
->ignore($this->user->profiles->first()->username),
'content' => 'required|min:3',
];
}
If necessary, you can create a FormRequest for validation and return
(new CreateProfileRequest())->rules()
in rules() , but the Livewire creators write that it is better not to use this way, as it is in their opinion redundant.
In my opinion, this option is more in line with SOLID and can be reused elsewhere.
In addition to options with separate methods and classes, there is a possibility to set special attributes to check field values:
namespace App\Livewire;
use Livewire\Attributes\Rule;
use Livewire\Component;
class CreateProfile extends Component
{
#[Rule(‘required|min:3’)]
public $name;
After that, it will be enough just to call $this->validate()
in the component method.
Plus, there are 2 additional ways to lock the property in the class to prevent a potential attacker from substituting an additional input into the form. We can do this by using the Locked attribute.
In this case, we do not give the possibility to modify the value of this property. Or we can specify the Eloquent model as the type of our property, as we did in our first code listing in this article.
In addition to all of this, we have the option of augmenting the data passed to the view with a second argument so that we don't have to use the component properties.
public function render()
{
return view('livewire.profile.show');
}
All possible methods for validation are listed here. In order to show validation errors, there is a similar to blade-templates directive @error.
The same logic works for file uploads as with normal inputs, however, we need to add a few lines of code for this. First - we need to add the WithFileUploads trait so that we can use the wire:model for file type inputs. After that, we will do the validation using the annotation
namespace App\Livewire;
use App\Models\Profile;
use Livewire\Attributes\Rule;
class CreateProfile extends Component
{
#[Rule('image|max:1024')] // 1MB Max
public $avatar;
public function create()
{
$path = $this->avatar->store('profile_avatars');
Profile::create([
'name' => $this->name,
'username' => $this->username,
'user_id' => $this->user->id,
'avatar' => $path,
]);
return redirect()->to('/profile');
}
Of course, in Livewire, you can also do multiple downloads and specify the file type when validating. For multiple uploads, the property should be searched through for each, validating the property this way: #[Rule([['images.*' => 'image|max:1024'])]
.
The file upload functionality is the same as Laravel itself, which means we can use the store method specifying the driver where we want to upload the file, for example, s3 -
$this->avatar->store('profile_avatars', 's3').
If you are uploading an image, you have the option to get a temporary link, as the application initially downloads a temporary copy of the file itself, and then uploads it to the required storage.
By default, the temporary storage is local, but you can change this by editing the temporary_file_upload.disk configuration key in config/livewire.php by publishing it in advance to the project using the php artisan livewire:publish --config console command
It is important to note that temporary images stored on s3 should be made public.
To move from a big component to a split structure, Livewire has a handy Form entity. To create it, just call the php artisan livewire:form ProfileForm
command. We can then put all the properties of the component into the form, and connect it via a special property, like this:
namespace App\Livewire\Forms;
use App\Models\User;
use Livewire\Attributes\Rule;
use Livewire\Form;
class ProfileForm extends Form
{
#[Rule('required|min:3')]
public $name = '';
#[Rule('required|unique:profiles:username|min:3')]
public $username = '';
#[Rule('image|max:1024')] // 1MB Max
public $avatar;
public ?string $path;
public ?User $user;
public function setUser(User $user)
{
$this->user = $user;
}
public function create()
{
$this->path = $this->avatar->store(‘profile_avatars’);
Profile::create([
'name' => $this->name,
'username' => $this->username,
'user_id' => $this->user->id,
'avatar' => $this->path,
]);
}
}
namespace App\Livewire;
use Livewire\Component;
use App\Models\Profile;
use App\Livewire\Forms\PostForm;
class CreateProfile extends Component
{
public ProfileForm $form;
public function mount()
{
$this->form->setUser(auth()->user());
}
public function save()
{
$this->form->create();
return $this->redirect('/profile');
}
public function render()
{
return view('livewire.profile.create');
}
}
With this separation of interaction with entities and frontend components proper, you can get an application structure where you don't have to duplicate code, and where it is easier to add new components. using the same forms.
As we looked at the beginning of the article - forms are not mandatory, but they also help to reuse already generated data.
For those backend engineers who rarely deal with frontend, the question may arise - how does it all work? To answer that, we need to understand what Hydrating is in the context of Livewire.
When you write components in PHP, you get the feeling that you are calling php functions directly from the page. Of course, this is an illusion. Under the hood, Livewire is a regular web application that simply sends Ajax requests when needed. With each Ajax request, Livewire must recreate the component based on snapshots that the application takes after each request to the server.
Each snapshot has several values - such as state - the current state of the component (its public properties), memo - information about the component itself to be recreated - such as its name, path, method, locale, children, etc.
You can see a snapshot of the component in the wire:snapshot attribute. When some function is called, the following request is sent to the livewire update URL:
{
calls: [
{ method: 'functionName', params: [] },
],
snapshot: {
state: {
param1: 123,
param2: 'anotherValue'
},
memo: {
name: 'component-name',
id: '123123',
},
}
}
The route used by Livewire is /livewire/update
If necessary, you can change the route that Livewire uses with the command
Livewire::setUpdateRoute(function ($handle) {
return Route::post('/any-other-route', $handle)
});
Of course, this is somewhat different from how hydrating works in modern front-end frameworks. Among the disadvantages of working with Livewire - constant Ajax requests to the server are needed to change the page state, as it is longer than comparing DOM with Virtual DOM.
As we have found out, Livewire is a convenient solution for backend-engineers who don't want to learn frontend for a long time and need to quickly make an application that will keep up with the modern times.
Livewire is convenient because it allows you to make a modern application almost seamlessly and quickly, in which all updates occur via Ajax requests, and page reloading is quite rare.
Of course, this kind of development will not replace a high-quality frontend engineer, but as a solution for a prototype or for an admin panel, it fits perfectly.
This is the first part of a small series of articles about Livewire for backenders. In the rest, we will also look at the Livewire functionality and how it works.