Back to Course |
Livewire 3 From Scratch: Practical Course

Parent-Children Form

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.

parent children product form


DB Structure

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]);

Livewire Component

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"
]
]
]