DEV Community

Cover image for How to create dynamic input fields with Laravel Livewire.
Jonathon Ringeisen
Jonathon Ringeisen

Posted on

How to create dynamic input fields with Laravel Livewire.

Hey there 👋, I recently ran into a situation where I needed to build a dynamic input field and thought I would share how I did it.

This how-to is going to cover how to generate an input field on demand, simply by clicking a link you'll be able to add an input field or remove the field. I'm also going to cover how to implement the validation for the dynamic inputs. So...lets get started.

I am going to assume that you already have your Laravel Livewire project setup.

Getting Started

The first thing you're going to want to do is create your Livewire component. You can do this by using the following command:

php artisan livewire:make DyanmicInputs
Enter fullscreen mode Exit fullscreen mode

This will create your DynamicInputs class and your dynamic-inputs blade template.

Building the class

Now that we have our files generated, let's go to the DynamicInputs class and start building this out.

The first method and property we want to add are.

// DynamicInputs.php

public Collection $inputs;

public function addInput()
{
    $this->inputs->push(['email' => '']);
}

// Taking advantage of Laravel Collections, we are simply 
// pushing an array with a key of email and an empty string 
// value to the inputs collection.

// This method will be called when we click the add input link.
Enter fullscreen mode Exit fullscreen mode

The next method we want to add will remove the input from the inputs collection.

// DynamicInputs.php

public function removeInput($key)
{
    $this->inputs->pull($key);
}
// Again, I'm using Laravel Collections here and I am using
// the pull method to remove the array with the specified key.

// This will be called when we click the remove input link.
Enter fullscreen mode Exit fullscreen mode

Next we want to add the mount method so that we can load our initial field on load.

//DynamicInputs.php

public function mount()
{
    $this->fill([
        'inputs' => collect([['email' => '']]),
    ]);
}

// I am using the Livewire fill method to populate the inputs
// collection when the page loads. This is how we display our
// initial input field.
Enter fullscreen mode Exit fullscreen mode

Creating the blade template

Now that we have most of the class done, let's dig into the blade template. I'm going to post the entire template below then we will go over whats going on.

<div class="max-w-xl w-full">
    @foreach($inputs as $key => $input)
    <div class="mt-12">
        <div class="w-full">
            <label for="input_{{$key}}_email" class="sr-only">Email</label>
            <input type="email" id="input_{{$key}}_email" wire:model.defer="inputs.{{$key}}.email" class="shadow-sm border-0 focus:outline-none p-3 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" autocomplete="off">
            @error('inputs.'.$key.'.email') <span class="text-xs text-red-600">{{ $message }}</span> @enderror
        </div>
        @if($key > 0)
        <div wire:click="removeInput({{$key}})" class="flex items-center justify-end text-red-600 text-sm w-full cursor-pointer">
            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
            <p>Remove Input</p>
        </div>
        @endif
    </div>
    @endforeach

    <div wire:click="addInput" class="flex items-center justify-center text-blue-600 text-sm py-4 w-full cursor-pointer">
        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"></path></svg>
        <p class="ml-2">Add New Input</p>
    </div>

    <div class="w-full flex justify-end mt-12">
        <button wire:click="submit" class="px-3 py-1 bg-blue-600 text-white rounded-lg">Submit</button>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode
// dynamic-inputs.blade.php

@foreach($inputs as $key => $input)

// This is a simple foreach loop that we use to iterate
// through the inputs collection. The $key is important!
Enter fullscreen mode Exit fullscreen mode
// dynamic-inputs.blade.php

<label for="input_{{$key}}_email" class="sr-only">Email</label>
<input type="email" id="input_{{$key}}_email" wire:model.defer="inputs.{{$key}}.email" class="shadow-sm border-0 focus:outline-none p-3 block w-full sm:text-sm border-gray-300 rounded-md" placeholder="you@example.com" autocomplete="off">

// When creating dynamic input fields, make sure your for
// attribute and id attribute are dynamic. I accomplish
// this by using the $key value with those attributes.

// Also, for wire:model you want to use inputs.{{$key}}.email
// as your value here. Inputs being the name of the collection
// $key being the dynamic value, and email being the
// collection key.
Enter fullscreen mode Exit fullscreen mode
// dynamic-inputs.blade.php

@if($key > 0)
    <div wire:click="removeInput({{$key}})" class="flex items-center justify-end text-red-600 text-sm w-full cursor-pointer">
        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
        <p>Remove Input</p>
    </div>
@endif

// We want to hide the remove input link on the first field.
// To do this, simply add an if statement that checks to see
// if the $key is > 0. If it is, then show it else don't.
Enter fullscreen mode Exit fullscreen mode
// dynamic-inputs.blade.php

<div wire:click="addInput" class="flex items-center justify-center text-blue-600 text-sm py-4 w-full cursor-pointer">
    <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"></path></svg>
    <p class="ml-2">Add New Input</p>
</div>

// And lastly we add the link to add the new input. Once
// You've add this you can go ahead and click it and you
// will be creating dynamic input fields.
Enter fullscreen mode Exit fullscreen mode

Let's go over how to implement validation for this.

To implement validation into this, you'll need to add the following code snippets.

// DynamicInputs.php

protected $rules = [
    'inputs.*.email' => 'required',
];

protected $messages = [
    'inputs.*.email.required' => 'This email field is required.',
];

public function submit()
{
    $this->validate();
}

// You'll notice we're using . notation to set our validation
// rules.
Enter fullscreen mode Exit fullscreen mode

Now let's add the validation to the front end. Below your input you'll want to add the following code.

// dynamic-inputs.blade.php

@error('inputs.'.$key.'.email') <span class="text-xs text-red-600">{{ $message }}</span> @enderror

// Notice the $key variable, you have to set this dynamically
// to make sure you catch the correct error for the correct
// input.
Enter fullscreen mode Exit fullscreen mode

I created a sandbox version of this here

I hope you enjoyed this article, feel free to comment below if you know of a way to improve this.

Top comments (11)

Collapse
 
bleriotnoguia profile image
Blériot Noguia • Edited

Thanks ! Brother. I noticed that when I click "Remove Input" and then try to add a new input again, the "Add New Option" button no longer works. I solved this issue by changing the div tag from "Add new option" with a button tag.

<button wire:click="addInput" class="flex items-center justify-center text-blue-600 text-sm py-4 w-full cursor-pointer">
    <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd"></path></svg>
    <p class="ml-2">Add New Option</p>
</button>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bhushancodesdot profile image
Bhushan • Edited

How Can fully Dynamic Validation in livewire Laravel

Controller

foreach ($request->all() as $key => $value) {
            if (strpos($key, 'textarea_') === 0 || strpos($key, 'textbox_') === 0 ) {
                $questionId = substr($key, strpos($key, '_') + 1);
                if($key == "textbox_78" || $key == "textbox_77"){
                     $rules['textbox_78'] = 'required|integer|min:1800|max:2024';
                     $rules['textbox_77'] = 'required|integer|between:1,12';
                     $messages['textbox_78' . '.required'] = 'This field is required';
                     $messages['textbox_78' . '.integer'] = 'Please enter an integer value';
                     $messages['textbox_78' . '.min'] = 'The value must be at least 1800';
                     $messages['textbox_78' . '.max'] = 'The value cannot exceed current Year';

                     $messages['textbox_77' . '.required'] = 'This field is required';
                     $messages['textbox_77' . '.integer'] = 'Please enter an integer value';
                     $messages['textbox_77' . '.between'] = 'Value between 1 and 12';
                     $messages['textbox_77' . '.size'] = 'size will 2 digit like 01';
                }else{
                     $rules[$key] = 'required';
                     $messages[$key . '.required'] = 'This field is required';
                }

            }
        }
Enter fullscreen mode Exit fullscreen mode

Blade file

 @if($question->type == 'textbox')
    <div class="grid grid-cols-12 gap-3 {{ $loop->even ? 'bg-white' : 'bg-darkbg' }}">
        <div class="col-span-6">
            <label class="block py-3 pl-4">{{ $question->name }}</label>
        </div>
        <div class="text-center  p-3"> 
            <input type="text" id="{{ $question->id }}" name="textbox_{{ $question->id }}" class="rounded-sm border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-black placeholder:text-black text-sm sm:leading-6">
        </div>  
        @if($errors->has('textbox_' . $question->id))
            <div class="error" style="color:red; margin-top: 55px; margin-left: -110px;">{{ $errors->first('textbox_' . $question->id) }}</div>
        @endif             
    </div>
@endif 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
willvincent profile image
Will Vincent

Good stuff Jonathon. Thanks for sharing

Collapse
 
jringeisen profile image
Jonathon Ringeisen

Thanks! Anytime.

Collapse
 
ilearnbydoing profile image
Durgesh Gupta

how to perform inline calculation i.e. in case of invoice qty*price to get inline total?

Collapse
 
jringeisen profile image
Jonathon Ringeisen

I'm actually building an invoicing feature that does just this. I have a collection called inputs and this is where all the input data lives. Then I do the following:

public function updatedInputs()
{
    $this->formatMappedInputs();
    $this->calculateTotals();
}

public function formatMappedInputs()
{
    $this->mapped_inputs = $this->inputs->map(function ($row) {
        $total = 0;
        $subtotal = 0;

        if ($row['price'] > 0) {
            $total += $row['price'];
            $subtotal += $total;
        }

        if (isset($row['quantity']) && $row['quantity'] > 0) {
            $total = $total * $row['quantity'];
            $subtotal = $total;
        }

        if (isset($row['discount']) && $row['discount'] > 0) {
            $total = $total - ($total * ($row['discount'] / 100));
        }

        return [
            'item' => $row['item'],
            'price' => $row['price'],
            'quantity' => $row['quantity'],
            'discount' => $row['discount'],
            'subtotal' => $subtotal,
            'total' => $total,
        ];
    });
}

public function calculateTotals()
{
    $subtotal = $this->mapped_inputs?->sum('subtotal');
    $discount = $this->mapped_inputs?->sum('subtotal') - $this->mapped_inputs?->sum('total');
    $total = $this->mapped_inputs?->sum('total');

    $this->totals = [
        'subtotal' => $this->formatCurrencyToUsd($subtotal),
        'discount' => $this->formatCurrencyToUsd($discount),
        'total' => $this->formatCurrencyToUsd($total),
    ];
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ilearnbydoing profile image
Durgesh Gupta • Edited

I used wire:change on blade

<input type="number" class="form-control form-control-sm text-center" 
id="input_{{$key}}_price" wire:model.defer="inputs.{{$key}}.price" 
wire:change="inlineTotal" >
Enter fullscreen mode Exit fullscreen mode

to call inlineTotal

  public function inlineTotal()
    {
        $this->inputs = $this->inputs->map(function ($item) {
            return [
                'title' => $item['title'],
                'description'  => $item['description'],
                'hsn_sac'  => $item['hsn_sac'],
                'bale_no'  => $item['bale_no'],
                'qty_per_box'  => $item['qty_per_box'],
                'no_of_boxes'  => $item['no_of_boxes'],
                'qty' => $item['qty'],
                'price' => $item['price'],
                'total_price' => $item['price'] * $item['qty'],
            ];
        });
    }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ilearnbydoing profile image
Durgesh Gupta

thanks

Collapse
 
mafalda2007 profile image
Mafalda

Muy bueno! Good!

Collapse
 
franco04508550 profile image
Franco

Very good but how could I dynamically enter that data in a save method?

Collapse
 
jaguar988 profile image
jaguar988 • Edited

Thanks Bro! but when i add js datepicker with inputs does not work for me