How to use @entangle to bind html attribute to a model property

Hello everyone,
Livewire is set on a brand new Laravel project with latest Laravel and Livewire
My Livewire project use a Model named cInput used through a livewire component named InputBox

What I’m trying to do is to get/set the inputbox.width html attribute by using the model ‘width’ property using the @entangle method (but in a pure livewire component, not using any blade component)

I’m pretty sure my notations/syntaxes in x-data and x-init are wrong and that is were I think I need your help guys. I’m in Livewire for just a week so please do not fire at me :slight_smile:

cInput Model has properties like this :

cInput.id
cInput.name
cInput.class
cInput.text
cInput.width    

Component View : \Resources\Views\Livewire\input-box.blade.php

<div x-data="{ top : $entangle(cInput.width) }" x-init="{ $watch('width', value => (document === $xrefs.container).width}"
class="relative z-0 inline-block">
<input 
	wire:click.prevent="CheckWidthSize($cInput)"
	name 	= "{{ $cInput->name }}"
	text 	= "{{ $cInput->text }}px;"
    class 	= "{{ $cInput->class }}" 
    x-ref 	= "myinputfield"
    width 	= x-bind:value="top" 
    id 		= "myinputfield"/>

Component “Controller” : \App\Http\LIvewire\InputBox.php

<?php
namespace App\Http\Livewire;
use App\Models\cInput;
use Livewire\Component;

class Builder extends Component
{
    public $cInput;
    protected $rules = ['$cInput.*.*' => '',];

    public function mount() {
       $this->cInput = cInput::all();	
    }
    public function CheckWidthSize($myInputs)
    {
    	$this->$cInputs = $myInputs;
        foreach ($this->cInput as $anInputbox) {
            // I'm trying to get the updated width value of the inputbox (its html width attribute)
            @dd($anInputbox->top);
        }
    }
    public function render()
    {
        return view('livewire.builder');
    }
}

?>

When the page is loaded with the component it receive the {{ cInput-> property notation but nothing from the entangled variable nor does the entangled variable be updated)

Also, I know that I achieve that by using a javascript eventlister on the change event (or so) on the inputbox and communicate the width to a method of my livewire component with @wire->emit but I really need to do it with the @entangle method in order to understand and better learn the way to use it.

Thank you very much for any help.

If all you want to do is have the width of the input equal the width field defined in your InputBox class, you can do so without involving AlpineJS. You’re making things much harder on yourself setting things up like this, but we’ll get it sorted!

First, a few recommendations:

  1. A lot of the “magic” that Laravel and Livewire do behind the scenes to make things work is based upon following certain conventions. One such convention is that your class name should match your file name. So I recommend renaming your Builder class to InputBox. In fact, Livewire may not work unless you do this.
  2. It’s my opinion that variable names should be as descriptive as possible. When you look back at your code 6 months from now, you’re not going to remember all the blood, sweat, and tears you put into making your code work. But you can take steps which make it easier on you or other developers down the line. So instead of shortening your variable name to $cInput, I would recommend writing out the full name, so you can know immediately what it is, even 6 months from now (it’s a good habit to develop, even if you never plan to look at this code again).
  3. Remove the $ from your variable name inside the $rules declaration. This is another one of those things based on convention.
  4. Remove the $ from the variable name when accessing it as a class field. In other words change this: $this->$cInputs = $myInputs; to this: $this->cInputs = $myInputs;. This is a PHP convention. Whenever you access a variable with $this leave the $ out of the variable name.
  5. You may need to remove the @ from @dd. I’m not sure what this is meant to be doing, but simply calling the dd function is enough.
  6. Your render function in your Livewire class is returning the wrong view. This is another Livewire feature based around convention. Your Livewire view is called input-box, not builder, so this will cause problems for you.
  7. This is a more general programming convention, but typically class methods start with a lowercase letter, and static methods start with a capital letter. This won’t cause problems with Livewire, but is another good programming habit to develop.
  8. Your Livewire view component should access class fields using the same $this syntax as you would use in the class itself. So change name = "{{ $cInput->name }}" to name = "{{ $this->cInput->name }}"
  9. You’re using some of your Alpine functions incorrectly. Change $entangle to @entangle. You could alternatively use $wire.entangle.
  10. You are trying to directly access cInput.width inside of your Alpine x-data. Keep in mine that AlpineJS and Livewire live in different worlds. They can only play together if you tell Alpine about Livewire using @entangle, or the $wire object. So until you entangle a livewire variable, alpine doesn’t know what cInput.width is.
  11. The way you are defining the width attribute of your input is incorrect. If you want to bind the width to an Alpine variable, do it like so: <input x-bind:[attribute]="[expression]">, so more specifically: x-bind:width="top" - Check out the AlpineJS documentation linked at the bottom of this post
  12. Another incorrect Alpine syntax is your ref accessor. Insted of $xrefs.container it should be $refs.container

Now for the implementation itself. I’m not entirely clear on what you’re trying to accomplish here, so you may need to provide more information if we are to find a solution that does what you want it to do. My understanding is that you are trying to render each element in $cInput as its own input field of varying widths. If so, you are going to need a loop to iterate over the variable in your view:

@foreach($this->cInput as $input)
    <input
       name="{{ $input->name }}"
       value="{{ $input->text }}px;" // You had this defined as text="", which is not a valid attribute. I don't know what this is meant to do, but it seems you are trying to set the value, so I changed to accordingly
       class="{{  $input->class }}"
       id="myInputField"
       width="{{ $input->width }}"
    />
@endforeach

This implementation will list out each element in $cInput as an input with properties matching those defined in the model. However, it will not synchronize changes to the input’s value with the model. ie. changing what’s in the input will not do anything - this is pretty much just a readonly field in this state. If you want the model instance backing each input tag to update when the input value is changed, you’re going to need to rethink your implementation. I would recommend rewriting the Livewire component to handle only one instance of $cInput at a time, rather than trying to manage them all at once in a collection. You can still use a collection if you like (check out the documentation in the link at the bottom of this post, 2nd code snippet under “Binding Directly To Model Properties”). I’ll show you the method I recommend because it’s easier to grok, but it’s up to you how you want to implement it.

cinputs.blade.php

Mounts the Livewire InputBox component for each instance in your cInput collection

<div>
    @foreach(\App\Models\cInput::all() as $cInput)
        {{-- The colon (:) before the cInput prop tells Livewire to interpret the expression as PHP, but you could also leave out the colon and put $cInput inside curly braces like normal: cInput="{{ $cInput }}" --}}
        <livewire:input-box :cInput="$cInput" />
    @endforeach
</div>

InputBox.php

The Livewire class containing the core logic for this component

<?php

namespace App\Http\Livewire;

use App\Models\cInput;
use Livewire\Component;

class InputBox extends Component
{
    public cInput $cInput;

    protected $rules = [
        'cInput.text' => 'required|string',
    ];

    public function mount(cInput $cInput)
    {
        $this->cInput = $cInput;
    }

    // Binding the input to the $cInput model is enough to have the input display the right value, and also update when the input value is changed, but you will need to save the model to persist those changes to your database.
    public function save()
    {
        // Validation is done here if you have some rules in place
        $this->validate();

        $this->cInput->save();

        // If you want the width of each input to change based upon the value you type into it, you could also update that here, and Livewire should re-render with the new width
        $this->cInput->update([
            'width' => $this->cInput->text,
        ]);
    }

    public function render()
    {
        return view('livewire.input-box');
    }
}

input-box.blade.php

The Livewire View component which models the cInput properties

<div>
    <input
        wire:model="cInput.text"
        name="{{ $this->cInput->name }}"
        class="{{ $this->cInput->class }}"
        width="{{ $this->cInput->width }}px"
        id="myInputField"
    />

    {{--  You will need some way of triggering the model to persist changes. Using a button to call the save function is easy and performant, but you could also call save everytime the input content changes using wire:change="save" - this will call the save method every time you type something in the input, which may impact performance if you do this too much..  --}}
    <button wire:click="save">Save</button>
</div>

I don’t have your models and data to test this code out with, but it should at least point you in the right direction. What will help you the most is taking a close look at the documentation for both Livewire and AlpineJS, which I’ve linked below.


Thank you SleeplessDev.

First of all, I really thank you for your time and efforts to help me here. I took the time to delve into all of this.

You are right about the notation conventions and also that I should rethink the implementation of my code here. I will do as you said.

So in that case and based on what you suggested, what would be the syntax for the @entangle variable part ? Is this correct ?

x-data="{ width : $entangle($this->cInput->width) }" x-init="{ $watch('width', value => (document === $refs.myinput.width}

(when added a x-ref=‘myinput’ to the element)

I know I’m asking a lots but if I could get the correct syntax in this context that would help me better understanding and going ahead and be able to use this technique whenever needed.

Thank you very much already and in advance too.
Anton.

You’re very welcome, I’m happy to help.

One important distinction to make is that Livewire and AlpineJS speak different languages, in order for Alpine to understand Livewire, you need a translator. This takes the form of specific interfaces (ex. the $wire global variable) or documented conventions (ex. @entangle($attributes->wire('model')) - this example uses PHP accessor syntax in JS, but can still be understood since this is a predefined convention). I can infer from your code that you haven’t yet grokked the distinction between the two. I’ll try to explain the underlying concepts as best I can, which I hope will help you better conceptualize this.

So in your code, there are a few things you are doing incorrectly:

  1. You are using $entangle() again instead of the correct syntax, either: @entangle() or $wire.entangle()
  2. You are slightly misunderstanding how to use @entangle (this is not a criticism, we ask for help in order to better understand). When we are entangling a variable, we are telling Alpine to make a copy of a certain variable’s value, and update that value any time the underlying variable changes. Therefore, when using entangle, we want to pass it the name of the variable to keep an eye on, not the value of that variable. With only the value, Alpine won’t be able to update its copy when Livewire’s version is changed. So in this case, change this: width : $entangle($this->cInput->width) to this: width : @entangle('width'). This is essentially saying "The JS variable defined here should always be equal to the Livewire variable called width".
  3. I understand that you are trying to change the width of the input boxes based on the value entered into that box. As I mentioned in my previous post, you could accomplish this without using Alpine at all, however, if you are dead set on using Alpine for this purpose, you can probably do away with the $watch declaration entirely. In your current setup what you are telling Alpine is "every time the JS variable called width changes, evaluate if document is equal to $refs.myinput.width". In other words, this isn’t actually doing anything useful. Try taking out the x-init section all together, and make the changes I mentioned above. Then, bind the width attribute of your input box the the AlpineJS variable called width we’ve defined in x-data, like so: x-bind:width="width". Alpine will be able to figure out what variable you are referencing here, provided you have defined a variable of that name in an x-data attribute before this component in the DOM hierarchy.

In summary, here is your entire code snippet updated with my suggestions:

// In your parent div
x-data="{ width : @entangle('width') }" 
// In your <input /> element
x-bind:width="width"

The Alpine docs don’t specifically mention Livewire because Alpine is a JS framework in its own right. But the Livewire docs do have a page describing how to use them together. The sections titled "Interacting With Livewire From Alpine: $wire" and "Sharing State Between Livewire And Alpine: @entangle" should be of particular interest to you.

Feel free to ask any additional questions you have. “Give a man a fish, and he will eat for a day. Teach a man to fish, and he will eat for a lifetime.”

Hello.
Perfect ! Thank you very very much for your time.
I managed to make things work with both strategies.
Thanks again.