livewire: Alpine does not initialise inside a blade conditional, after re-render.

This applies to a page re-render only, after some logic is performed. For example:

<button wire:click="setModelToTrue()">click</button>

@if($model->itIsTrue())
<div x-data="{ open: false }">
  <button @click="open = true">Show More...</button>
  <ul x-show="open" @click.away="open = false">
    <li>
      <button wire:click="archive">Archive</button>
    </li>
    <li>
      <button wire:click="delete">Delete</button>
    </li>
  </ul>
</div>
@endif

On a page load/full refresh with “itIsTrue()” all works as expected.

However, If we click the button to turn “it to true” dynamically, the re-render results in the alpine component not being initialized and the dropdown does not work.

I have tried livewire keys, element ID’s, extra div elements (as in the troubleshooting section) but I just can’t get anything working.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 35 (2 by maintainers)

Commits related to this issue

Most upvoted comments

So I have found out that I can initialize the new Alpine component using:

Alpine.discoverUninitializedComponents(function(el){ Alpine.initializeComponent(el) })

We can do this automatically after the dom is updated:

window.livewire.hook('afterDomUpdate', () => {
  window.Alpine.discoverUninitializedComponents((el) => {
    window.Alpine.initializeComponent(el)
  })
})

It has taken me close to a full day to get to this solution. Considering Livewire and Alpine are supposed to place nice together, maybe this should be considered to be part of the core or at least of the docs.

@calebporzio, what do you think? I am happy to PR the docs if you agree.

What I’ve ended up doing is adding a wire:key to the Alpine component which fixed the issue.

I’m running into some similar issues with Livewire re-rendering the component. @Dimitris947 have you also used this with Alpine v3? Because I get discoverUninitializedComponents is not a function.

Well, this issue do I also get a lot. The trick is to wrap a div around it like this:

<div>
     @if($model->itIsTrue())
        ...
    @endif
<div>

And in my instance I even had to go as crazy as below to get it working with a foreach:


<div>
     @if($this->order)

         <div id="update-status-{{ $this->order->id }}">

             @foreach(...)

                 <button x-on:click="open = false; window.livewire.find( '{{ $this->orderItemLivewireId }}' ).call('setStatus', '{{ $status}}');">
                       {{ $status}}
                 </button>

             @endforeach

        </div>
    @endif
<div>

In my instance, window.livewire.find would always call the orderItemLivewireId which was first clicked. This happened multiple times now to me in my project.

But above code would solve it. My guess is that Alpine is not able to reregister x-on events correctly or something. I believe this is related in our case: https://github.com/alpinejs/alpine/issues/412

Fun fact: before I got it working like above, I could even inspect the DOM, and see the correct orderItemLivewireId’s within those livewire.find() calls … But whichever id was first called via the livewire.find, would fire always, even if the dom did not match.

But I am pretty sure you could get it working with some unique id’s here and there, and not having to resort to manually booting Alpine components.

@calebporzio if you are interested in more sample code to reproduce this behavior, I am happy to provide it.

@adddz couple of things you can do.

If the component is in a loop You can use the $loop parameter that Laravel has available for you: wire:key="{{ $loop->iteration }}". Or if you’re looping over posts you can just use the model ID like so: wire:key="{{ $post->id }}"

If the component is not in a loop and you’re using the same component multiple times Just use something manual, like wire:key="thing-1" and wire:key="thing-2" etc.

I recently got weird behaviour because I had something within a blade @if statement. In that case it can help to wrap that @if statement in a <div>. Also see https://laravel-livewire.com/docs/2.x/troubleshooting

I have the same issue, a component that does something on x-init based on what it receives from x-data, passing a Livewire property. Also, doing: document.addEventListener("livewire:load", function(event) { window.livewire.hook('afterDomUpdate', () => { window.Alpine.discoverUninitializedComponents((el) => { window.Alpine.initializeComponent(el); }); }); }); does not work for me.

I have the similar issue when the whole component is refreshed using @this.call('$refresh'); and the code inside x-initis never triggered.

@calebporzio I can confirm this works with your example, but also that it does not work in my codebase, using the latest Livewire and Alpine as of 7/19/20. The only material differences I can spot are that my component has a function-call style data provider, and an init method, and the init method is async (speaks to camera input). Putting an x-data on the Livewire parent div definitely causes the component initializer to run, but the x-shows and such on the child elements do not work.

The only thing that resolves this is this code, from @booni3

window.livewire.hook('afterDomUpdate', () => { window.Alpine.discoverUninitializedComponents((el) => { window.Alpine.initializeComponent(el) }) })

Until this is fixed, another simple work around, not better than the above, just super quick and simple, is to leave the component there on the page, and toggle display: none.

Note, I’m sure everyone is used to it but the css for display none comes from tailwindcss and is the “hidden” in the x-input.date element.

Code example:

<x-input.date field="dateOfBirth" class="{{ ($this->type == 'refill' ? 'hidden': '') }}"/>

here is the blade component with the JS, I like flatpickr better than pickaday, it also doesn’t use moment.js, but it doesn’t work well on brave browser “right now”, and date controls often behave weird on mobile devices, so the idea is if it’s on a mobile device, just default to the device date implementation

@props([
  'field',
  'label'=> (isset($field) ? str($field)->snake()->replace('_',' ')->title() : ''),
  'placeholder' => 'your '. str($field)->snake()->replace('_',' '). ' ...',
  'error' => ($errors->has($field) ? 'error' : ''),
  'hasError' => $errors->has($field),
  'message' => ($errors->has($field) ? '<br>' . $errors->first($field) : ''),
])

@if(isMobile())
  <label class="block {{ $attributes->get('class') }}">
    <div class="form-label {{ $error }}">{{ $label }}:{!! $message !!}</div>
    <div class="relative w-full">
      <input type="date" wire:model.lazy="{{ $field }}" name="{{ $field }}" id="{{ $field }}"
             class="form-input-date block w-full pr-7 {{ $error }}"
             placeholder="{{ $placeholder }}" {{ $attributes->except('class') }}/>
      <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
        <svg class="{{ ($hasError ? 'text-red-400' : 'text-gray-400') }} fill-current h-4 w-4" viewBox="0 0 20 20">
          <path
            d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
            clip-rule="evenodd" fill-rule="evenodd"></path>
        </svg>
      </div>
    </div>
  </label>

@else

  <label
    x-data
    x-init="flatpickr($refs.input, {dateFormat: 'M d, Y'})"

    class="block {{ $attributes->get('class') }}">
    <div class="form-label {{ $error }}">{{ $label }}:{!! $message !!}</div>
    <div class="relative">
      <input type="text" wire:model.lazy="{{ $field }}" data-input
             x-ref="input"
             class="form-input block w-full {{ $error }}"
             placeholder="{{ $placeholder }}" {{ $attributes->except('class') }} />
      <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
        <svg class="{{ ($hasError ? 'text-red-400' : 'text-gray-400') }} fill-current h-4 w-4" viewBox="0 0 20 20">
          <path
            d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
            clip-rule="evenodd" fill-rule="evenodd"></path>
        </svg>
      </div>
    </div>
  </label>


@endif


@section('stylesheets')
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
@endsection

The wire:key solution on the same element that defines the x-data attribute works but it kind of got buried in the conversation.

@adddz couple of things you can do.

If the component is in a loop You can use the $loop parameter that Laravel has available for you: wire:key="{{ $loop->iteration }}". Or if you’re looping over posts you can just use the model ID like so: wire:key="{{ $post->id }}"

If the component is not in a loop and you’re using the same component multiple times Just use something manual, like wire:key="thing-1" and wire:key="thing-2" etc.

I recently got weird behaviour because I had something within a blade @if statement. In that case it can help to wrap that @if statement in a <div>. Also see https://laravel-livewire.com/docs/2.x/troubleshooting

Thank you. I agree that if you don’t set the wire:key and there’s an @if conditional, some weird behavior might happen.

@adddz see below (note the wire:key). Note that this does not work when you define x-data outside the component as a function within <script></script>.

<div>
    @if ($thingIsShown)
        <div x-data="{ open: false }" wire:key="theThing">
            <button @click="open = true">Show More...</button>
            <ul x-show="open" @click.away="open = false">
                <li>
                <button wire:click="archive">Archive</button>
                </li>
                <li>
                <button wire:click="delete">Delete</button>
                </li>
            </ul>
        </div>
    @endif

    <button wire:click="showThing">foo</button>
</div>

REOPEN

Hey just added some similar code in a blade component to another project and this is still an issue 😦

Newest Livewire and alpine…

@if($attributes->has('long-running'))
  <span x-data="{ loading: false }">
    <button

      x-on:click="{ loading=true; }"
      x-bind:disabled="loading"
      type="{{ $type }}" {{ $attributes->merge(['class' => $colors[$color]]) }} {{ $attributes->except('class') }} >
      <span x-cloak x-show="loading"><x-icon.spinner/></span>
      <span class="px-2">{{ $slot }}</span>

    </button>
  </span>
@else
  <button
    type="{{ $type }}" {{ $attributes->merge(['class' => $colors[$color]]) }} {{ $attributes->except('class') }} >
    {{ $slot }}
  </button>
@endif

This works beautifully, if the component is visible on the page at the outset, however in some cases like a modal, that is added to the page with a @if loading is never set to false, and the button renders with the spinner and doesn’t disable.

I know this is an old issue so I’m just putting this out here as there isn’t an example that breaks every time.

Have you verified you are including @livewireScripts BEFORE including AlpineJS in your app layout?

@bnrosa same here this does not work for me. But the CSS “hidden” trick by @roni-estein works for me very well.