Advanced Livewire: JavaScript Component Layer

By @samuel · 2021-10-02 17:11

Advanced Livewire: JavaScript Component Layer

This is a continuation of my Advanced Livewire series started in the previous post: Advanced Livewire: Traits.

Here we'll talk about working with your components in JavaScript.

Few people know this, but your Livewire components actually have a 1:1 "mirror" in JavaScript — any PHP property that's public is stored on the frontend, and any method that's public can be called from JS. Or more specifically, few people know how structured the JS layer is.

Accessing the Livewire component in JavaScript

To fetch the frontend instance of a LW component, you can click the element with wire:id in Chrome dev tools and then use this in the developer console:

$0.__livewire

($0 refers to the last selected element.)

This will return a Component instance.

Accessing the component data

If you type:

$0.__livewire.data

You'll see all of the component data.

Accessing the component methods

And similarly, if you use:

$0.__livewire.call('foo', 'bar');

This will call foo('bar') in PHP — using an HTTP request. The method itself returns a promise and its return value is passed to the then() callback.

This has both cool use cases and potential security issues when you're not aware of this. So always make sure that your public properties and methods really should be public.

Here's an example of how the returned promise can be used:

public function slug(string $text): string
{
    return Str::slug($text);
}
$0.__livewire.call('slug', 'foo bar').then(text => console.log(text));

// prints 'foo-bar' in the developer console

Using the Alpine proxy

The code above is useful, but also kind of ugly. For that reason, we will use the proxy created for Alpine. Odds are that we'll be using Alpine anyway, so this makes things even simpler. To access the proxy, use:

$0.__livewire.$wire;

The proxy lets you access properties like this:

$0.__livewire.$wire.foo;

Set them like this:

$0.__livewire.$wire.foo = 'bar';

And call methods like this:

$0.__livewire.$wire.foo('bar');

From here on we'll assume that we're specifically in Alpine context, so we'll just use $wire in Alpine to access this proxy.

Usage in Alpine

You can access $wire like any other Alpine property:

<div x-data>
    <button @click="$wire.foo = 'bar'">Make it bar</button>
</div>

In methods we use this, again like for any other Alpine properties:

<div x-data="{
    foo: '',

    bar() {
        this.$wire.foo(this.foo);
    },
}">
    <input x-model="foo">
    <button @click="bar">Bar</button>
</div>

Use case on this forum

When you submit a reply, this method gets executed:

reply() {
    $wire.reply().then(() => this.replying = ! $wire.valid)
},

And similarly, the edit profile component looks like this:

<aside x-data="{
    editing: false,

    save() {
        $wire.save().then(() => this.editing = ! $wire.valid)
    },
}">
    <form
        x-on:submit.prevent="save()"
        class="overflow-hidden bg-white rounded-lg shadow"
        wire:loading.class="cursor-wait"
  >
      ...
    </form>
<form>

The benefit of using slightly more intelligent Alpine logic is that the UI interactions feel much smoother.

In this specific example, the UI works in these steps:

  1. The user clicks a button
  2. The UI freezes — the input becomes gray and disabled, and hovering over it changes the cursor to a loading spinner
  3. The response comes back
    • If it contains any validation errors, the UI unfreezes and the error is displayed
    • If there are no errors, the reply form animates away and the user's reply appears next to the other replies

We use the same logic on the edit profile page:

<aside x-data="{
    editing: false,

    save() {
        $wire.save().then(() => this.editing = ! $wire.valid)
    },
}">
  <form
      x-on:submit.prevent="save()"
      class="overflow-hidden bg-white rounded-lg shadow"
  >
      ...
    </form>
</aside>

And that's it for this week's Advanced Livewire. I have decided to make it a weekly series, so make sure to check back next week. (Or see the newsletter, since it's also getting sent in each issue of our weekly newsletter.)

See you next week!

  • By @wizjo · 2022-06-06 05:47 (edited)

    What is benefit of doing like this, instead of using wire:click="save" and entangling "editing" value? Or is the aim to show that you can, not necessary that it is better?

    • By @samuel · 2022-06-06 07:48

      Yeah, the point is to show that it can be used. In some contexts it is better and it lets you do things you couldn't do otherwise. And most importantly, it makes you aware of the security implications — that any of your public methods or properties can be accessed from the frontend, even if you don't expose them in Blade.

      • By @wizjo · 2022-06-06 07:59

        Thank you and hope you will cover this topic (this special cases where we can benefit from calling components this way) and show some examples in future episodes =).

  • By @abrardev99 · 2021-10-04 07:19

    Great read.

  • By @lars · 2021-10-02 19:01

    Great stuff, adding some additional (but not required JS) can really improve the UX in some cases! Good example 🙌🏼

  • By @boris · 2021-10-02 17:28

    Great read! Is $0 a Livewire-specific feature?

    • By @samuel · 2021-10-02 17:39

      It's not! That's just the last element you clicked on.

      See here:

      Screenshot

      It's like right clicking an element in the Elements tab and selecting "Store as a global variable", except you can save those clicks if you just need to work with the last element.

      $0 is the last one, $1 is the previous one, $2 is the one selected before the previous one, etc.

      • By @wali · 2021-10-13 14:46

        In cases when you have accessed devtools via shortcut or via browser menu. $0 will refer to the body element