The second demo project out of four is a parent-children form. It's a typical form, for example, for invoices or orders with products.
In this form, we will be able to add products to the list or remove them from a list.
First, let's quickly see what the Migration and Model look like.
database/migrations/xxx_create_products_table.php:
Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->integer('price'); $table->timestamps();});
app/Models/Product.php:
use Illuminate\Database\Eloquent\Casts\Attribute; class Product extends Model{ protected $fillable = [ 'name', 'price', ]; protected function price(): Attribute { return Attribute::make( get: fn($value) => $value / 100, set: fn($value) => $value * 100, ); }}
And a seeder to have some products:
Product::create(['name' => 'iPhone', 'price' => 999.99]);Product::create(['name' => 'iPad', 'price' => 699.99]);Product::create(['name' => 'iMac', 'price' => 1999.99]);
Now let's create a Livewire component.
php artisan make:livewire ParentChildren
Next, let's think about what properties we will need. Obviously, for customer name and email. Then we will need one for all the products, which will be a collection. And lastly, we will need a property to store products in the order, an array with a structure of ['product_id' => '', 'quantity' => 1]
.
In the mount
method, of course, we need to get all the products and will set the first order product where product_id
will be empty and quantity of 1.
app/Livewire/ParentChildren.php:
use App\Models\Product;use Illuminate\Support\Collection; class ParentChildren extends Component{ public string $customer_name = ''; public string $customer_email = ''; public array $orderProducts = []; public Collection $allProducts; public function mount(): void { $this->allProducts = Product::all(); $this->orderProducts[] = ['product_id' => '', 'quantity' => 1]; } // ...}
Now that we have all products and where to store selected product, let's make a form.
resources/views/livewire/parent-children.blade.php:
<form wire:submit="save"> <div> <label for="customer_name" class="block font-medium text-sm text-gray-700">Customer name</label> <input id="customer_name" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="text" wire:model="customer_name" /> </div> <div class="mt-4"> <label for="customer_email" class="block font-medium text-sm text-gray-700">Customer email</label> <input id="customer_email" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="email" wire:model="customer_email" /> </div> <div class="block mt-4 w-full border border-gray-300 rounded-md shadow-sm"> <div class="bg-gray-100 px-4 py-3"> Products </div> <div class="min-w-full align-middle p-4"> <table class="min-w-full divide-y divide-gray-200 border"> <thead> <tr> <th class="px-6 py-2 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Product</span> </th> <th class="px-6 py-2 bg-gray-50 text-left"> <span class="text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">Quantity</span> </th> <th class="px-6 py-2 bg-gray-50 text-left"> </th> </tr> </thead> <tbody class="bg-white divide-y divide-gray-200 divide-solid"> @foreach($orderProducts as $index => $orderProduct) <tr> <td class="px-2 py-2"> <select wire:model="orderProducts.{{ $index }}.product_id" id="orderProducts[{{ $index }}][product_id]" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"> <option value="0">-- choose product --</option> @foreach($allProducts as $product) <option value="{{ $product->id }}">{{ $product->name }} ({{ number_format($product->price, 2) }})</option> @endforeach </select> </td> <td class="px-2 py-2"> <input wire:model="orderProducts.{{ $index }}.quantity" id="orderProducts[{{ $index }}][price]" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="number" /> </td> <td class="px-2 py-2"> <button type="button" class="px-3 py-2 bg-red-600 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500"> Delete </button> </td> </tr> @endforeach </tbody> </table> <button type="button" class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> + Add Another Product </button> </div> </div> <button class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> Save Product </button></form>
The form should look similar to this:
For the customer fields, there's nothing special. Just a simple wire:model
to the customer's public property.
The important part here is how to use wire:model
for the orderProducts
property. The main part here is the key
of an array. When adding the key
of an array to the wire:model
Livewire knows which array value to update.
A very similar way is to remove products from the list. We will add a removeProduct
method to accept an array's key
. In that method, because orderProducts
is a simple array, we can use PHP unset
function to remove that key
from an array. After removing the product from the list, we must re-index array values.
app/Livewire/ParentChildren.php:
class ParentChildren extends Component{ public string $customer_name = ''; public string $customer_email = ''; public array $orderProducts = []; public Collection $allProducts; public function mount(): void { $this->allProducts = Product::all(); $this->orderProducts[] = ['product_id' => '', 'quantity' => 1]; } public function removeProduct($index): void { unset($this->orderProducts[$index]); $this->orderProducts = array_values($this->orderProducts); } // ...}
resources/views/livewire/parent-children.blade.php:
// ...<tbody class="bg-white divide-y divide-gray-200 divide-solid"> @foreach($orderProducts as $index => $orderProduct) <tr> <td class="px-2 py-2"> <select wire:model="orderProducts.{{ $index }}.product_id" id="orderProducts[{{ $index }}][product_id]" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm"> <option value="0">-- choose product --</option> @foreach($allProducts as $product) <option value="{{ $product->id }}">{{ $product->name }} ({{ number_format($product->price, 2) }})</option> @endforeach </select> </td> <td class="px-2 py-2"> <input wire:model="orderProducts.{{ $index }}.quantity" id="orderProducts[{{ $index }}][price]" class="block mt-1 w-full border-gray-300 rounded-md shadow-sm" type="number" /> </td> <td class="px-2 py-2"> <button type="button" class="px-3 py-2 bg-red-600 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500"> <button wire:click="removeProduct({{ $index }})" type="button" class="px-3 py-2 bg-red-600 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500"> Delete </button> </td> </tr> @endforeach</tbody>// ...
Adding a new product to the list is the same as in the mount
method adding a new array value with empty product_id
and quantity
of 1.
app/Livewire/ParentChildren.php:
class ParentChildren extends Component{ public string $customer_name = ''; public string $customer_email = ''; public array $orderProducts = []; public Collection $allProducts; public function mount(): void { $this->allProducts = Product::all(); $this->orderProducts[] = ['product_id' => '', 'quantity' => 1]; } public function addProduct(): void { $this->orderProducts[] = ['product_id' => '', 'quantity' => 1]; } public function removeProduct($index): void { unset($this->orderProducts[$index]); $this->orderProducts = array_values($this->orderProducts); } // ...}
resources/views/livewire/parent-children.blade.php:
// ... <div class="min-w-full align-middle p-4"> <table class="min-w-full divide-y divide-gray-200 border"> // ... </table> <button type="button" class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> <button wire:click="addProduct" type="button" class="mt-4 px-4 py-2 bg-gray-800 rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700"> + Add Another Product </button></div> // ...
After selecting products and quantity and submitting the form, you will have the structure of data as follows:
array:3 [▼ // app/Livewire/ParentChildren.php:37 "customer_name" => "test" "customer_email" => "test@test.com" "orderProducts" => array:3 [▼ 0 => array:2 [▼ "product_id" => "1" "quantity" => 1 ] 1 => array:2 [▼ "product_id" => "2" "quantity" => "2" ] 2 => array:2 [▼ "product_id" => "3" "quantity" => "3" ] ]]