Alpine: Display an element of the DOM outside of x-data

Created on 5 Mar 2020  路  13Comments  路  Source: alpinejs/alpine

Hi :),

I would like to know how to display an element of the DOM outside of x-data. For the example of README.md, it works well:

<div x-data="{ open: false }">
    <button @click="open = true">Open Dropdown</button>

    <ul
        x-show="open"
        @click.away="open = false"
    >
        Dropdown Body
    </ul>
</div>

But I wanted to know if it is possible to do something like this:

    <div x-data="{ open: false }">
        <button @click="open = true">Open Dropdown</button>
    </div>

    <ul
            x-show="open"
            @click.away="open = false"
    >
        Dropdown Body
    </ul>

Thank you :)

question

Most helpful comment

Alpine directives only work in Alpine components and a components is only what you get inside the outerHTML of a tag having an x-data attribute.
Hugo's suggestion are valid but I assume you are more interested in knowing a solution when your 2 components are far away in the DOM.

The preferred way to achieve intra component communication in Alpine is via native events.
You dispatch an event from a component and you listen for the event in the other one.

Something like the snippet below would do.

<div x-data="{id: 1}">
    <button @click="$dispatch('open-dropdown',{id})">Open Dropdown</button>
</div>

<ul x-data="{ open: false }"
    x-show="open"
    @open-dropdown.window="if ($event.detail.id == 1) open = true"
    @click.away="open = false">
    Dropdown Body
</ul>

Note, I've added an id variable in case you need to manage multiple dropdown in the same page.

All 13 comments

@seb-jean you should wrap in another container (div, template) and put the x-data on the ancestor of wherever you want to use it.

Alternatively you can move the ul inside your div that has x-data on it.

Alpine directives only work in Alpine components and a components is only what you get inside the outerHTML of a tag having an x-data attribute.
Hugo's suggestion are valid but I assume you are more interested in knowing a solution when your 2 components are far away in the DOM.

The preferred way to achieve intra component communication in Alpine is via native events.
You dispatch an event from a component and you listen for the event in the other one.

Something like the snippet below would do.

<div x-data="{id: 1}">
    <button @click="$dispatch('open-dropdown',{id})">Open Dropdown</button>
</div>

<ul x-data="{ open: false }"
    x-show="open"
    @open-dropdown.window="if ($event.detail.id == 1) open = true"
    @click.away="open = false">
    Dropdown Body
</ul>

Note, I've added an id variable in case you need to manage multiple dropdown in the same page.

@SimoTod thx for that example!

How would I catch this event in a JavaScript component?

I'm using this to trigger the event (using jQuery because my framework is based on that):

function InvoiceItem() {
  return {
    ...
    setTotal() {
      $(this.$el).trigger('changed');
      this.total = this.round(this.gross * this.amount);
    },
  }
}

And this to catch the event in Invoice(), not InvoiceItem():

function Invoice() {
  $(document).on('changed', function(e) {
    console.log('changed!', e);
  });

  return {
    items: [1, 2, 3],
    addItem() {
      this.items.push("x");
    },
    bar() {
      console.log('bar');
    }
  }
}

This works, but I don't know how to fire Invoice.bar(); Thx!

You can use the document modifier to listen for any event triggered by other javascript libraries.
I assume Invoice is an Alpine component so you are going to have something like

<div x-data="Invoice()" @changed.document="bar()">
   ...
</div>

You can't access bar() from the outside because this exist only in the data context of your alpine component.
If you want to do something more complex where you call bar from random point of your code, you need to refactor it and make bar is a function available in your global scope.

Hi @SimoTod thx, I understood that. I just wanted to know if there is a way to move the @changed.document="bar" event listener into my Invoice() component's JavaScript code - not in the HTML of the component. And also I'd like to move the event triggering part into the JS part to keep things together where they logically belong.

This is what I'm tryting to do:

<table x-data="Invoice()">
  <template x-for="item in items" :key="item">
    <tr x-data="InvoiceItem()"><tr> [...] <td><input x-model="amount" @input="setAmount($event.target.value"></td></tr>
  </template>
</table>
// InvoiceItem is the single invoice item row
function InvoiceItem() {
  return {
    net: 100,
    gross: 120,
    amount: 1,
    [...]

    // setNet(), setGross()

    setAmount(val) {
      this.amount = val;
      this.setTotal();
    },
    setTotal() {
      this.total = this.round(this.gross * this.amount);
      this.$dispatch('update-totals'); // here I'd like to trigger the foo event
    },
  }
}

// invoice holds all single rows and one overall total
function Invoice() {
  return {
    total: 0,
    items: [1, 2, 3],

    addItem() {
      this.items.push(this.items.length); // add next number
    },

   // here I want to define a method that fires on every update of any invoice item
    @update-totals() {
        // loop all items and calc sum of total prices
    }
  }
}

In this instance you can fall back to using document.addEventListener() in the x-init of the second component

Thx @HugoDF . Maybe I'm getting you wrong, but this does not change anything of the following requirement, does it?

And also I'd like to move the event triggering part into the JS part to keep things together where they logically belong.

I want both the trigger and the listen to be in the JS code, not in any markup.

The reason is that I'm using a PHP framework and IMHO using attributes on some elements is not where the logic happens. I have 3 inputfields for every invoice item (component InvoiceItem()): NET, GROSS, AMOUNT. All of them are bound to JS properties and I have setNet, setGross, setAmount methods to update the other properties accordingly AND calc the total price.

Now I want to trigger the update of the parent component (Invoice()) whenever the price of an item changes. I understood that I could add @input events to each of the three input elements and I could add @item-changed listener to the Invoice() DOM element. But I'd really prefer to have that triggering and catching in my JS code and not in the HTML.

Hope that clarifies things.

x-init can reference a function, say you call it "init".

In that "init" function you can add the event listener on the document.

I don't think you can achieve that "easily" in Alpine.

If the separation of concerns is one of the main priorities, i would asses if a different library fits your case better (i know, that's not the response that you want it but going against the library design is going to be hard).

Alpine design deeply relies on html attributes. Listener that can access the internal data directly can only be defined as html attribute.

It's maybe possible to "bend" the rules (in your function you can create the object first, pass it to the listener and only at the end you return it. I haven't tried though, not sure if the reactivity would work) but you probably shouldn't because on a long run you will end up with a lot of additional code so you might as well use your own custom html element (there's a good example using the salesforce membrane).

Hi @SimoTod , thx, that answer was very helpful and in fact exactly what I wanted to hear, because I try to get the concepts of Alpine and see what is possible and how things are done or should be done :)

I've just created this codepen: https://codepen.io/baumrock/pen/JjdzJej - It would be great to get an idea of how I can make the overall total react on all changes of the single invoice items?

Thx again for your help! I really appreciate it 馃憤

Your case is a bit complex.

Intra-communication using events works okay when it's a 1-to-1 thing but your Invoice component depends on a function of all your invoice lines every time it updates.

This is not immediate because each component is supposed to be unaware of the number and structure of other components in the same page so my gut feeling is that it should be modelled as a single component.

Something like https://codepen.io/SimoTod/pen/yLNwomY

Be aware that there's a lot of duplication because I wrote that really quickly.

Even if your grand total is visually placed in a different point of your page, I will still create a component containing all the lines. That component will be responsible for computing the grand total when something changes and firing a 'refresh-grandtotal' event containing the result.
The grand total component will just listen for that event on the document object, read the value in the event payload and update his label.

As I said, the codepen is just a quick example, there may be a better way to do it and it surely needs tidying up, but I hope it can help you.

Closing since it looks like the problem was sorted, feel free to reopen or start a discussion thread.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andruu picture andruu  路  3Comments

ryangjchandler picture ryangjchandler  路  3Comments

imliam picture imliam  路  5Comments

adevade picture adevade  路  3Comments

BernhardBaumrock picture BernhardBaumrock  路  3Comments