I am dipping my toe into Livewire, and I am finding it extremely heavy-going. It often does not work, and the documentation promise of speedy development is, for me, not being fulfilled. I am not sure whether I should just back out the work and use standard full page round trips, as I cannot afford this sluggish DX.
I have this form, which is a regular Laravel template (some form elements removed for brevity):
resources/views/page-generation.blade.php
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Title generation') }}
</h2>
</x-slot>
<div class="py-12">
<form class="flex max-w-7xl mx-auto sm:px-6 lg:px-8"
style="{{ dev_border('green') }}"
wire:submit="generate_titles">
<div class="mt-16 w-1/2">
<div class="w-full max-w-lg" style="{{ dev_border('red') }}">
<div class="flex flex-wrap -mx-3 mb-6">
<div class="w-full px-3">
<label class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2" for="product-name">
Product/service name
</label>
<input class="appearance-none block w-full bg-gray-200 text-gray-700 border border-gray-200 rounded py-3 px-4 mb-3 leading-tight focus:outline-none focus:bg-white focus:border-gray-500"
id="product-name"
type="text"
placeholder="XPR Widget Supreme">
<p class="text-gray-600 text-xs italic">This is the main product or service being offered</p>
</div>
</div>
<!-- Skipped several form elements -->
</div>
</div>
<div class="mt-16 w-1/2" style="{{ dev_border('orange') }}">
<!-- This renders just the button to start with i.e. no results -->
<div class="mt-2">
@livewire(GenerateTitles::class)
</div>
</div>
</form>
</div>
</x-app-layout>
I have this Livewire template:
resources/views/livewire/generate-titles.blade.php
<div>
<button class="mt-2 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-md">
Generate titles
</button>
<!-- Only render if there are titles to show -->
@if($titles)
<div class="mt-2 mb-2">
<p class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Suggested titles
</p>
<div class="rounded-md"
style="padding: 8px 8px 8px 24px; background-color: rgb(229 231 235);">
<ol class="list-decimal" style="">
@foreach($titles as $title)
<li>
{{ $title }}
</li>
@endforeach
</ol>
</div>
</div>
<div class="mt-2 mb-2">
<p class="block uppercase tracking-wide text-gray-700 text-xs font-bold mb-2">
Call to action
</p>
<p class="mt-2 mb-2"
style="padding: 8px 8px 8px 24px; background-color: rgb(229 231 235);">
{{ $cta }}
</p>
</div
@endif
</div>
When I click on the <button>
the page redraws entirely, which is obviously not want I want; I expect the form submit event to be caught, and an AJAX operation to occur. I want to access the form elements inside PHP.
The initial rendering of the button by @livewire(GenerateTitles::class)
does work (this is the bit I want to replace dynamically). However in my browser tools I can see the <form>
does not have a JS event attached.
I also have a component:
app/Livewire/GenerateTitles.php
<?php
namespace App\Livewire;
use App\Foo\Bar as MyBar;
use Livewire\Component;
class GenerateTitles extends Component
{
// Inputs
public string $productName;
public string $targetAudience;
public string $usps;
public string $shortCta;
public string $keyPhrases;
// Outputs
public array $titles;
public string $cta;
protected MyBar $myBar;
/**
* DI for this Laravel component
*
* H/T to https://dev.to/iamkirillart/how-to-implement-dependency-injection-in-laravel-livewire-con
*/
public function boot(MyBar $myBar): void
{
// ...
}
public function generate_titles(): void
{
$response = $this->runQuery();
$this->titles = $response['titles'];
$this->cta = $response['cta'];
}
protected function runQuery(): array
{
return $this->myBar->runTitleGeneration();
}
public function render()
{
return view('livewire.generate-titles');
}
}
The Livewire script is indeed injected at the end of the page.
However if I visit /profile
and see the auto-generated Profile page, I can see the forms themselves are Livewire components:
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
<livewire:profile.update-profile-information-form />
</div>
</div>
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
<livewire:profile.update-password-form />
</div>
</div>
<div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="max-w-xl">
<livewire:profile.delete-user-form />
</div>
</div>
</div>
In my browser dev tools, I can see that the <form>
elements have JS handlers.
Do I need to render my form in Livewire in order to make it work? There is nothing dynamic in the form, so this would seem like an artificial restriction.
(I accept that I may have scanned the docs too quickly: the curse of trying to spin up on a technology without wanting to watch endless videos).
Originally I think I had wire:click
on the button, and the button was outside of the form. This did work, in the sense that it did an AJAX operation without any manual wiring, but with the button being outside of the form, I could not work out how to obtain the client-side form contents to send to the server so it can be processed using PHP.
This perhaps supports my theory in the title: the button had an event handler automatically attached because it was rendered inside a Livewire template.
Since the form is - as you say - in a regular Livewire template, therefore not being a Livewire component, it will ignore the wire:submit="generate_titles" attribute, plus the generate_titles() method would also not referenceable because in another component. Therefore the button triggers a regular submission of the form which produces an update of the entire page.
The solution is to transform the form in a Livewire component so it can manage a "wire:submit" event with its own "manageSubmit()" method, the inputs in the form must also be wired to some component's properties:
<input class="appearance-none block w-full ....."
id="product-name"
type="text"
wire:model="productName"
placeholder="XPR Widget Supreme"
>
$productName
must be a public property in the related class of the "form" component