Search code examples
javascriptlaravellaravel-bladelaravel-livewirealpine.js

Appending alpine js variable to laravel blade component's attribute


I'm trying to append the value inside the alpine JS variable to Laravel blade component's attribute

I'm using laravel 10, Alpine V3 and Livewire V3 Here's my code :

<template x-for="(field, index) in fields" :key="index">
    <tr>
        <td x-text="index+1"></td>
        <td>
            <x-forms.input wire:model="journals.index.account" type="text"></x-forms.input>
        </td>
        <td>
            <x-forms.input wire:model="journals.index.amount" type="text"></x-forms.input>
        </td>
        <td><button type="button" class="btn btn-danger btn-small" @click="removeField(index)">&times;</button></td>
    </tr>
</template>

The x-forms.input component :

<div>
    <input
        type="{{ $type }}"
        class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
</div>

And here's my alpine code :

<script>
    function handler () {
        return {
          fields: [],
          addNewField() {
              this.fields.push({
                  txt1: '',
                  txt2: ''
               });
            },
            removeField(index) {
               this.fields.splice(index, 1);
             }
          }
    }
</script>

My Livewire class :

class CreateJournal extends Component
{
    public $journals = [];

    public function save()
    {
        foreach ($this->journals as $journal) {
            \App\Models\Payable::create($journal);
        }
    }
}

The intended result is to append the index variable of alpine to the blade wire:model attribute like so :

<x-forms.input model="journals.**index**.account" type="text"></x-forms.input>

How can i achieve this ?


Solution

  • I suppose that wire:model is evaluated before Alpine objects are instantiated, so it can't interact with Alpine's variables.

    We could use @entangle to use an Alpine variable that references a Livewire variable, but since version 3 of Livewire it is possible to use the magic property $wire to directly reference the Livewire component, so we can use this feature.

    From your example the relationship between the Livewire journals property and the Alpine fields[] property is not very clear, I will assume that they refer to the same data and therefore I will do without fields[]. I also assumed that your addNewField() and removeField() will handle rows, not fields (this was a bit cryptic)

    The Livewire class:

    <?php
    
    namespace App\Livewire;
    
    use Livewire\Component;
    
    class TheClass extends Component
    {
        // these are example data
    
        public $journals = [
    
            ['account' => 'acc001', 'amount' => 10] ,
            ['account' => 'acc002', 'amount' => 20]
        ];
    }
    

    The view:

    <div x-data="handler">
    
        <table>
    
            <template x-for="(row, index) in $wire.journals" :key="row.rowKey">
    
                <tr>
    
                    <td x-text="index + 1"></td>
    
                    <td>
                        <x-forms.input x-model="row.account" type="text" />
                    </td>
    
                    <td>
                        <x-forms.input x-model="row.amount" type="text" />
                    </td>
    
                    <td>
                        <button type="button" 
                                class="btn btn-danger btn-small"
                                @click="removeRow(index)"
                        >
                            &times;
                        </button>
                    </td>
    
    
                </tr>
    
            </template>
    
        </table>
    
        <br>
    
        <button @click="addNewRow()">
            [ADD ROW]
        </button>
    
        <button @click="$wire.$refresh">
            [REFRESH]
        </button>
    
        <br>
        <br>
     
        // the foreach is for debugging 
    
        @foreach($journals as $row)
    
            @php($rowId = $row['rowKey'] ?? ' ')
    
            <div>
                {{ "($rowId) - ({$row['account']}) - ({$row['amount']})" }}
            </div>
    
        @endforeach
    
        <script>
    
            function handler () {
    
                return {
    
                    init() {
                        let rowKey = Date.now();
                        this.$wire.journals.forEach (el => {typeof el.rowKey == "undefined"  &&  (el.rowKey = rowKey++)});
                    },
    
                    addNewRow() {
    
                        this.$wire.journals.push ({
    
                            account: '',
                            amount: '',
                            rowKey: Date.now()
                        });
                    },
    
                    removeRow(index) {
                        this.$wire.journals.splice (index, 1);
                    }
                }
            }
    
        </script>
    
    </div>
    

    In the x-for loop I referenced the journals property via $wire.

    I also added to each row a "unique" key (in the init() and addNewRow() methods) to use as :key because when a row is deleted using splice(), the indexes are compacted and this can confuse x-for. Consider whether to modify this functionality and for example manage the key in the backend (you can use an id if it already exists).

    I also inserted a @foreach for debugging purposes only: by clicking on the REFRESH button the data is sent to the backend and the component is updated accordingly, showing the changes.

    Edit

    Here a working version of the input Blade component:

    <div>
        <input {{ $attributes->merge([
                    'class' => 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg ' .
                               'focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 ' .
                               'dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 ' .
                               'dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500'
                  ])
               }}
        >
    </div>