Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Aaron1178's avatar

Alpine Scope? issue.

Hey guys,

I'm having what I would assume is an alpinejs scoping issue with x-data.

I have a tab system like so:

<div class="flex-row mx-4 w-full">
    @include($currentPricingTab)
</div>

and in my tabs, I have the following:

tab1

<div class="flex-row" x-data="labour">
<x-table>
    <x-slot:heading>
        <x-table.heading>Item</x-table.heading>
        <x-table.heading>Trade</x-table.heading>
        <x-table.heading>Type</x-table.heading>
        <x-table.heading>Hours</x-table.heading>
        <x-table.heading hidden>Meters</x-table.heading>
        <x-table.heading>Rate</x-table.heading>
        <x-table.heading>Total</x-table.heading>
        <x-table.heading></x-table.heading>
    </x-slot:heading>


    <template x-for="(e, index) in items" :key="index">
        <x-table.row x-on:click.outside="cancelEditingState(index)" x-on:dblclick="editRow(index)" x-on:keydown.enter="cancelEditingState(index)">
            <x-table.cell class="w-4/12">
                <div x-show="e.editing"><x-wire-input x-model='e.item.description'></x-wire-input></div>
                <div x-text="e.item.description" x-show="!e.editing"></div>
            </x-table.cell>

            <x-table.cell class="w-32">
                <div x-show="e.editing">
                    <x-wire-native-select :options="['ea', 'lm', 'm2', 'm3']" x-model="e.item.unit" class="!text-black"/>
                </div>
                <div x-text="e.item.unit" x-show="!e.editing"></div>
            </x-table.cell>

            <x-table.cell class="w-56">
                <div x-show="e.editing">
                    <x-wire-inputs.currency placeholder="0.00" prefix="$" x-model="e.item.unit_price" x-on:input="calculateTotal()" />
                </div>
                <div x-show="!e.editing" class="inline-flex">$<div x-text="e.item.unit_price"></div></div>
            </x-table.cell>

            <x-table.cell class="w-32">
                <div x-show="e.editing"><x-wire-input x-model='e.item.quantity' x-on:input="calculateTotal()"></x-wire-input></div>
                <div x-text="e.item.quantity" x-show="!e.editing"></div>
            </x-table.cell>

            <x-table.cell class="w-56">
                <div x-show="e.editing">
                    <x-wire-inputs.currency placeholder="0.00" prefix="$" x-model="e.item.total" />
                </div>
                <div x-show="!e.editing" class="inline-flex">$<div x-text="e.item.total"></div></div>
            </x-table.cell>

            <x-table.cell>
                <div><x-wire-button x-on:click="deleteRow(index)">-</x-wire-button></div>
            </x-table.cell>
        </x-table.row>
    </template>

    <x-table.row @click.stop="">
        <x-table.cell>
            <x-wire-button primary class="rounded-full h-5" x-on:click="addRow">+</x-wire-button>
        </x-table.cell>
    </x-table.row>

</x-table>
</div>

@script
<script>
    Alpine.data('labour', () => ({
        'items': @entangle('labourItems'),
        'currentlyEditing': @entangle('lCurrentlyEditing'),

        addRow() {
            console.log('cakked');

            if (this.items.length > 0 && this.isItemEmpty(this.items[this.currentlyEditing].item)) {
                return;
            }

            let newRowItem = {
                'editing': true,
                'item': {
                    'description': '',
                    'unit': '',
                    'unit_price': null,
                    'quantity': null,
                    'total': null,
                },
            };

            this.items.push(newRowItem);

            this.editRow(this.items.length - 1);

            this.$dispatch('row-added');
        },

        changeEditStatus(index) {
            if (this.currentlyEditing !== index) {
                this.items[this.currentlyEditing].editing = false;
                this.currentlyEditing = index;
            }
        },

        isItemEmpty(item) {
            for (let key in item) {
                if (item.hasOwnProperty(key) && item[key]) {
                    return false;
                }
            }
            return true;
        },

        cancelEditingState(index) {
            this.recalculateTotalPrice();

            if (this.isItemEmpty(this.items[index].item)) {
                this.items.pop(this.currentlyEditing);
                this.currentlyEditing--;
                return;
            }

            if (this.currentlyEditing === index) {
                this.items[this.currentlyEditing].editing = false;
            }
        },

        editRow(index) {
            this.recalculateTotalPrice();

            if (this.items.length <= 0) {
                return;
            }

            if (this.items[this.currentlyEditing]) {
                this.items[this.currentlyEditing].editing = false;
            }

            if (this.items[index]) {
                this.items[index].editing = true;
            }

            this.currentlyEditing = index;
        },

        deleteRow(index) {
            this.recalculateTotalPrice();

            if (this.items[index]) {
                this.items.splice(index, 1);

                if (this.items.length <= 0) {
                    this.currentlyEditing = 0;
                    return;
                }

                this.currentlyEditing--;

            }
        },

        calculateTotal() {
            if (this.items[this.currentlyEditing]) {
                const total = this.items[this.currentlyEditing].item.unit_price * this.items[this.currentlyEditing].item.quantity;
                this.items[this.currentlyEditing].item.total = total;
            }
        },

        recalculateTotalPrice() {
            this.$wire.call('recalculateTotalPrice');
        }
    }))
</script>
@endscript

tab2

<div class="flex-row" x-data="materials">
    <x-table>
        <x-slot:heading>
            <x-table.heading>Item</x-table.heading>
            <x-table.heading>Unit</x-table.heading>
            <x-table.heading>Unit Price</x-table.heading>
            <x-table.heading>Qty</x-table.heading>
            <x-table.heading>Total</x-table.heading>
            <x-table.heading></x-table.heading>
        </x-slot:heading>

        <template x-for="(ie, index) in items" :key="index">
            <x-table.row x-on:click.outside="cancelEditingState(index)" x-on:dblclick="editRow(index)" x-on:keydown.enter="cancelEditingState(index)">
                <x-table.cell class="w-4/12">
                    <div x-show="ie.editing"><x-wire-input x-model='ie.item.description'></x-wire-input></div>
                    <div x-text="ie.item.description" x-show="!ie.editing"></div>
                </x-table.cell>

                <x-table.cell class="w-32">
                    <div x-show="ie.editing">
                        <x-wire-native-select :options="['ea', 'lm', 'm2', 'm3']" x-model="ie.item.unit" class="!text-black"/>
                    </div>
                    <div x-text="ie.item.unit" x-show="!ie.editing"></div>
                </x-table.cell>

                <x-table.cell class="w-56">
                    <div x-show="ie.editing">
                        <x-wire-inputs.currency placeholder="0.00" prefix="$" x-model="ie.item.unit_price" x-on:input="calculateTotal()" />
                    </div>
                    <div x-show="!ie.editing" class="inline-flex">$<div x-text="ie.item.unit_price"></div></div>
                </x-table.cell>

                <x-table.cell class="w-32">
                    <div x-show="ie.editing"><x-wire-input x-model='ie.item.quantity' x-on:input="calculateTotal()"></x-wire-input></div>
                    <div x-text="ie.item.quantity" x-show="!ie.editing"></div>
                </x-table.cell>

                <x-table.cell class="w-56">
                    <div x-show="ie.editing">
                        <x-wire-inputs.currency placeholder="0.00" prefix="$" x-model="ie.item.total" />
                    </div>
                    <div x-show="!ie.editing" class="inline-flex">$<div x-text="ie.item.total"></div></div>
                </x-table.cell>

                <x-table.cell>
                    <div><x-wire-button x-on:click="deleteRow(index)">-</x-wire-button></div>
                </x-table.cell>
            </x-table.row>
        </template>

        <x-table.row @click.stop="">
            <x-table.cell>
                <x-wire-button primary class="rounded-full h-5" x-on:click="addRow">+</x-wire-button>
            </x-table.cell>
        </x-table.row>

    </x-table>
</div>

@script
<script>
    Alpine.data('materials', () => ({
        'items': @entangle('materialItems'),
        'currentlyEditing': @entangle('mCurrentlyEditing'),

        addRow() {
            if (this.items.length > 0 && this.isItemEmpty(this.items[this.currentlyEditing].item)) {
                return;
            }

            let newRowItem = {
                'editing': true,
                'item': {
                    'description': '',
                    'unit': '',
                    'unit_price': null,
                    'quantity': null,
                    'total': null,
                },
            };

            this.items.push(newRowItem);

            this.editRow(this.items.length - 1);

            this.$dispatch('row-added');
        },

        changeEditStatus(index) {
            if (this.currentlyEditing !== index) {
                this.items[this.currentlyEditing].editing = false;
                this.currentlyEditing = index;
            }
        },

        isItemEmpty(item) {
            for (let key in item) {
                if (item.hasOwnProperty(key) && item[key]) {
                    return false;
                }
            }
            return true;
        },

        cancelEditingState(index) {
            this.recalculateTotalPrice();

            if (this.isItemEmpty(this.items[index].item)) {
                this.items.pop(this.currentlyEditing);
                this.currentlyEditing--;
                return;
            }

            if (this.currentlyEditing === index) {
                this.items[this.currentlyEditing].editing = false;
            }
        },

        editRow(index) {
            this.recalculateTotalPrice();

            if (this.items.length <= 0) {
                return;
            }

            if (this.items[this.currentlyEditing]) {
                this.items[this.currentlyEditing].editing = false;
            }

            if (this.items[index]) {
                this.items[index].editing = true;
            }

            this.currentlyEditing = index;
        },

        deleteRow(index) {
            this.recalculateTotalPrice();

            if (this.items[index]) {
                this.items.splice(index, 1);

                if (this.items.length <= 0) {
                    this.currentlyEditing = 0;
                    return;
                }

                this.currentlyEditing--;

            }
        },

        calculateTotal() {
            if (this.items[this.currentlyEditing]) {
                const total = this.items[this.currentlyEditing].item.unit_price * this.items[this.currentlyEditing].item.quantity;
                this.items[this.currentlyEditing].item.total = total;
            }
        },

        recalculateTotalPrice() {
            this.$wire.call('recalculateTotalPrice');
        },
    }))
</script>
@endscript

When the livewire component loads, and say tab1 is loaded by default, the tab 1 alpinejs functionality works as expected, where as if I go to tab2 from tab1, the alpinejs functionality doesn't work. An the vice versa happens if tab2 is loaded by default.

I know it has something to do with x-data, as the secondary tab (not the default loaded tab) is referencing the primary tabs 'items' array.

Any help would be appreciated +1

0 likes
1 reply
Aaron1178's avatar

I ended up fixing it. In the end I found it to be a livewire issue caused by an oversight by myself. I solved it by adding wire:key to the tabs root div element.

Please or to participate in this conversation.