How to use Alpine.data() in @push within Laravel Blade components?

Viewed 1214

With Alpine.js 2 it was possible to develop Blade components with the following structure:

...
<div x-data="myApp()">
...
@once
    @push('child-scripts')
<script>
    function myApp() {
        return {
            ...
        }
    }
</script>
    @endpush
@endonce

This was great in that it allowed all the code for the component to be defined together.

In Alpine.js 3 this approach of defining functions is deprecated (https://alpinejs.dev/upgrade-guide#alpine-data-instead-of-global-functions). Instead, they should be defined like this:

...
<div x-data="myApp">
...
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('myApp', () => ({
            ...
        }))
    })
</script>

However, it states "you need to define Alpine.data() extensions BEFORE you call Alpine.start()". This suggests that with the new approach it is impossible (or at best unreliable) to use @push to add data functions for use in Blade components and, therefore, impossible to keep all the code for a Blade component together.

My code all works fine with Alipne 3, if I use the deprecated approach. If I try to include the new syntax data function via @push, I get "myApp is not defined" JS error from my Blade component.

I am using Alpine.js 3.9 installed as an NPM module. I have also tried using Alpine.js 3 via <script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>. The result is the same in both cases.

Is there a way to keep all the code together in the Blade component?

UPDATE 25.02.2022

It seems I need to provide more details of what I am doing so here they are.

The relevant bits of my app.blade.php are:

<head>
    ...
    <script src="{{ asset('js/app.js') }}" defer></script>
    @livewireStyles
    ...
</head>
<body>
    // HTML/Blade stuff here
    @livewireScripts
    @stack('child-scripts')
</body>

My app.js is like:

import Alpine from 'alpinejs'
require('./bootstrap');
window.Alpine = Alpine
Alpine.start();

I have also tried:

require('./bootstrap');
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start();

My Blade component is like this:

...
<div x-data="myApp">
...
@once
    @push('child-scripts')
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('myApp', () => ({
            ...
        }))
    })
</script>
    @endpush
@endonce

When I access a page that uses the Blade component, I get "Uncaught ReferenceError: myApp is not defined".

If I revert my Blade component to the old syntax (as follows) it works with no errors:

...
<div x-data="myApp()">
...
@once
    @push('child-scripts')
<script>
    function myApp() {
        return {
            ...
        }
    }
</script>
    @endpush
@endonce

The only difference between the working and failing versions is the way in which myApp is defined. The deprecated method works but, with the new method, it doesn't.

2 Answers

Not sure why you would think that using @push is unreliable.

Since the @push directive (like any other blade directive) is evaluated by the blade engine on the server-side, by the time the page loads, anything you push is already there on the page.

One thing to note is that it is important to defer the execution of the javascript assets so that it is executed after the document has been parsed. You can do that by adding a defer attribute like so:

<!DOCTYPE html>
<html>
    <head>
        ...
        <script src="{{ mix('js/app.js') }}" defer></script> {{-- or CDN here --}}
    </head>
    <body>
        ...
        @stack('scripts')
    </body>
</html>

// resources/js/app.js

import Alpine from 'alpinejs'

window.Alpine = Alpine

Alpine.start()

I'm already using Alpine v3 this way without any issues.

Unfortunately or not, P. K. Tharindu is right. I try his recommandation (put defer on script witch AlpineJs..), and worked. Thank

Related