Search code examples
javascriptphphtmlalpine.jslaravel-10

Alpine.js: Add form fields with x-model


I would like to try implement alpine.js framework into my existing native javascript function. I have an array of addresses (which contain street, postcode, etc) and it has an added function so that I can add multiple addresses for one customer. The code below is when I am using native javascript which works as desired:

<div>
    <form id="customerForm" method="post" action="{{ route('customers.store') }}">
        @csrf
        <div class="mb-4">
            ...
        </div>
        <div class="mb-4">
            ...
        </div>
        <div id="addressFields">
            <label>Address</label>
            <div class="address-fields">
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][street1]" placeholder="Street 1" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][street2]" placeholder="Street 2" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][postcode]" placeholder="Postcode" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][city]" placeholder="City" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][state]" placeholder="State" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[0][country]" placeholder="Country" />
                </div>
            </div>
        </div>
        <button type="button" id="addAddress">Add Address</button><br><br>
        <button type="submit">Create Customer</button>
    </form>
</div>

<script>
    document.addEventListener('DOMContentLoaded', function() {
        let addressIndex = 1;
        // Function to add address fields dynamically
        function addAddressFields() {
            const addressFields = document.getElementById('addressFields');
            const addressContainer = document.createElement('div');
            addressContainer.classList.add('address-fields');
            addressContainer.innerHTML = `
                <br/>
                <div>
                <label>Address ${addressIndex}</label>
                <button class ="removeAddress" id="removeAddress">Remove</button>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][street1]" placeholder="Street 1"/>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][street2]" placeholder="Street 2"/>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][postcode]" placeholder="Postcode"/>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][city]" placeholder="City"/>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][state]" placeholder="State"/>
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${addressIndex}][country]" placeholder="Country"/>
                </div>
            `;
            addressFields.appendChild(addressContainer);
            addressIndex++;
        }

        document.getElementById('addAddress').addEventListener('click', function() {
            addAddressFields();
        });

        document.addEventListener('click', function(event) {
            if (event.target.classList.contains('removeAddress')) {
                event.preventDefault();
                event.target.closest('.address-fields').remove();
                addressIndex--;
            }

        });
    });
</script>

Now, I already tried to use alpine.js based on my original code. There are some issue with it. Based on what I did, when I create customer, the addresses details only shows the latest addresses input. Meaning, for example, I fill up the addresses, then I want to add another one. So there are two addresses. When I create customer, it only display one addresses (which is the latest).

<div x-data="addresses: []">
    <form id="customerForm" method="post" action="{{ route('customers.store') }}">
        <div>
            ...
        </div>
        <div>
            ...
        </div>
        <template x-for="(address, index) in addresses" :key="index">
            <div>
                <label x-text="'Address ' + (index + 1)"></label>
                <div>
                    <input type="text" name="addresses[${index}][street1]" placeholder="Street 1"
                        x-model="address.street1" />
                </div>
                <div>
                    <input type="text" name="addresses[${index}][street2]" placeholder="Street 2"
                        x-model="address.street2" />
                </div>
                <div>
                    <input type="text" name="addresses[${index}][postcode]" placeholder="Postcode"
                        x-model="address.postcode" />
                </div>
                <div>
                    <input type="text" name="addresses[${index}][city]" placeholder="City" x-model="address.city" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${index}][state]" placeholder="State"
                        x-model="address.state" />
                </div>
                <div class="address-field mb-2">
                    <input type="text" name="addresses[${index}][country]" placeholder="Country"
                        x-model="address.country" />
                </div>
            </div>
        </template>
        <button type="button" @click="addresses.push({})">Add Address Alpinejs</button>
    </form>
</div>

I need help on how I can improve and fix my code. Thank you very much for your help and much appreciated.

This is when I add addresses

This is what has been displayed


Solution

  • You want to directly bind to the parent. You should look at the data from the x-for as read only to make it make more sense.

    Example:

    <div x-data="{ data: [{ firstName: '', lastName: '', id : Date.now()}] }" style="margin-top: 20px">
      <button @click="data.push({ firstName: '', lastName: '', id: Date.now()})" style="margin-bottom: 10px">Add Row</button>
      <table>
        <template x-for="(obj, index) in data" :key="obj.id">
          <tr>
            <td><input type="text" x-model="data[index]['firstName']"></td>
            <td><input type="text" x-model="data[index]['lastName']"></td>
            <td><button @click="data.splice(index, 1)">Remove Item</button></td>
          </tr>
        </template>
      </table>
    </div>
    

    Code taken from this CodePen.