Basically, I'm doing a tabs "plugin" and need to use tab.scrollIntoView(), but I can't use it as is, as alpine still has the tab element hidden (x-show'd out).
I'm currently using a setTimeout but I feel it should/could be cleaner
<div x-data="{tabs: tabs('tab1')}">...</div>
function tabs(initialTab) {
return {
current: initialTab
select(tab) {
// tab is a dom element
// I'm using id to progressively enhance from anchor links
// and limit markup duplication
this.current = tab.id
// I want to do something like this
$nextTick(() => tab.scrollIntoView({behavior: "smooth"}))
// I currently do this, but it feels icky
setTimeout(() => tab.scrollIntoView({behavior: "smooth"}), 1)
},
// ...
}
}
I tried to pass the $dispatch to x-data (x-data="{tabs: tabs('tab1', $dispatch)}") but as I expected it doesn't work.
I wonder if at least some of the magic properties could be stuck on the Alpine object for this use case ?
Why are you using object scope with external function call? You can use <div x-data="tabs('inital')>...</div>, where tabs - function which returns scope. In this case you get access for this.$dispatch and this.$nextTick and other things.
docs
Why are you using object scope with external function call ?
Kind of namespacing to prevent potential collisions. I simplified this example by keeping only the tabs part, but my data object can look more like x-data="{tabs: tabs('tab1'), otherPlugin: otherPlugin(), foo: 'bar', ...}", and then I could use x-show="tabs.isShown('tab1')" or the likes. I didn't really think about it being a potential problem. Not my first error on "this" :-)
I'm happy to know there is an easy fix, thank you @rzenkov.
But I still wonder what to do in case of property collisions and/or code namespacing/organisation. I can imagine it being messy when you compose a lot of those "plugin functions".
I leave @calebporzio and the other maintainers to decide if this is a discussion they want to have. Feel free to close the issue if not.
@Lelectrolux As rzenkov pointed out, mixing global and component scoped functions is not easy.
It's a massive simplification, but you can assume that you have access your scope variables and magic properties only when they show explicitly in the attribute string value of a x-* directive.
(Pay attention with x-data, though, that one is evaluated before the component finishes its set up phase so you can't really access other "scope" variables or magic properties from there).
The string is what is passed to the magic Alpine evaluator but the body of an external function will run on a different context so if you want to use a magic property there, you have to pass it in.
For example, you can have
<div x-data="{tabs: tabs('tab1') }" >
<button @click="tabs.select('tab1', $dispatch)">click me</button>
<!-- Note, dispatch is passed into the function from here -->
</div>
<script>
function tabs(initialTab) {
return {
current: initialTab,
select(tab, $dispatch) {
console.log(tab, $dispatch)
},
}
}
</script>
and it would work.
Long story short:
I hope it makes sense to you.
@SimoTod Yeah, as soon as I read rzenkov answer I got what I did wrong. Painfully obvious once you see it, considering the way Alpinejs works under the hood.
I already refactored my "plugin" by dropping the "namespacing" I was doing and went on with my day.
Name clashing was a problem that probably won't happen anyway in my case, and it's not a problem future me and a wrapping div won't be able to solve, I guess.
But still I miss the readability it added (easy to differentiate what came from the plugin with the tabs prefix everywhere in the dom.
I actually already tried what you proposed and it worked, but having to pass manually the $dispatch each time to the function is a non-starter.
I still wonder if there is a smart api someone closer to the source could think about to deal with that.
But like I said, feel free to close this.
Hmm, minute ago i found that $dispatch accessible only from template expression. Not shure we need access this.$dispatch within "function scope()", But why not?
How about this?
// in Component constructor
// Binded to this.$el
unobservedData.$dispatch = this.getDispatchFunction(el);
One purpose for this.$dispatch binded to this.$el - inter component messaging, in other cases we don't need that, or need custom element.dispatchEvent(/* */). Even if we need message to other component with additional reaction in current component we can call $dispatch in template and add listener on current component. Therefore only one purpose for this.$dispatch - add some consistency.
yeah, I suppose it would make sense to be able to call this.$dispatch to dispatch from the root element from an external function.
Like I think someone else mentioned, there would be a name collision with the magic $dispatch that gets added to the template string.
If it's as simple as adding a default $dispatch to the constructor of the component class AND still having the magic one attached to template expressions, that'd be great.
If there are weird gotchas or it's any more complex, we maybe should just punt on it.
Thoughts?
Yes,it is not as simple as it seemed at first. unobservedData.$dispatch overlaps evaluated $dispatch.
This behavior leads to saferEvalNoReturn, where it calls new Function and with block, where vars defined in $data takes precedence over additional vars.
I don't see simple way to avoid this behavior. (excepting reassigning $dispatch in $data within each saferEvalNoReturn call, but only for those expressions, which uses $dispatch).
IMHO Such mess up is not worth the end result, but docs apparently should reflect this.
Moving discussion here from the issue @SimoTod mentioned above.
Couple of options & thoughts here:
When using the $dispatch helper in an inline expression, the target is automatically assigned to the current target variable (specifically for event handlers such as x-on:click). In this case, when somebody wants to use the $dispatch in an external event handler, can we not pass it through alongside the $event variable with the target already set in the same way? Obviously, you could do this manually with @click="handler($event, $dispatch)" too. I think I'd opt for this if we're not too worried about having access to $dispatch at the parent / root level.
With regards to @calebporzio 's thoughts on adding a default $dispatch to the component data, I think that would be the smartest move. When the expression is evaluated inside of the component, i.e. a click handler or something similar, can the $dispatch not be overwritten with the one that gets added to the template string? I don't see there being a huge problem with this approach other than the overwriting of $dispatch causing the proxy handler to re-evaluate and re-render which if this is a problem, can be circumvented inside of the setter.
It makes sense to me.
The problem with the second point was about avoiding that the generic dispatch would override the inline ones. At the moment, it would do because of the structure of the safeEval function.
We can avoid that but i think we need to use a neated 'with'.
They could be 2 different PRs anyway.
Yeah. I'll throw in a PR to pass the $dispatch to event handler with the event target set by default, see what @calebporzio thinks. Then we can look at the 2nd option too.
Thanks for looking into this @ryangjchandler.
Being new to Alpine I will say the $dispatch and $event not being available in the function component was not intuitive.
I updated this CodePen with examples of all the magic variables being accessed inside a function: https://codepen.io/atomgiant/pen/ExVYdXz
All of them appear to work fine except for $event and $dispatch. The $event one seems surprising so maybe I am missing something?
My expectation was $event would reference the current event and $dispatch would dispatch based on the current $event.target which I believe lines up nicely with @ryangjchandler proposal.
If the overriding of the $dispatch is too much of an issue I'm not sure this is possible but something like $el.dispatch or $event.dispatch (assuming I can access $event) would have been fine for my needs.
@atomgiant The $event variable gets passed to your handler as the first parameter.
handler($event) {
...
}
Should work fine currently. You have to make sure that you're doing @click="handler" and not @click="handler()".
We can maybe update the documentation to make clear what is available and when as well as adding the functionality Ryan is working on.
We can maybe update the documentation to make clear what is available and when as well as adding the functionality Ryan is working on.
Yeah for sure. I'm wondering now whether the documentation is growing a little too big in the README, @calebporzio any plans for a dedicated Alpine site yet?
I'll be sure to update the README when I make my PR.
Thanks @ryangjchandler. The @click=“handler” without parens was the missing link for me. I’d be happy to help with a PR for the doc Readme if you’re interested
Yeah, feel free to make the PR. Im not sure whether it makes more sense to put it under the x-on section or under the $event section. Surprise us!
I'm not sure if my comment fits here.
I "need" the $dispatch function in custom Modal.js file because this is what I want to achieve:
// what I want
<button @click="Modal.open('modal-1')">Show modal 1</button>
// what I'm doing
<button @click="$dispatch('modal:open modal-1')">Show modal 1</button>
<div id="modal-1" x-data={...}>
Some modal here that listen to the event "modal:open modal-1"
</div>
As you see is not a big deal but it would be nice something global like Alpine.$dispatch maybe?
Anyway, awesome work caleb.
If you are already in the real javascript part, you can trigger an event in the canonical way. $dispatch is just an utility helper for the html around a standard event dispatching.
const event = new CustomEvent('modal:open:modal-1');
document.dispatchEvent(event)
<button x-data @click="Modal.open('modal-1')">open</button>
<div id="modal-1" x-data="{...}" @modal:open:modal-1.document="...">
...
</div>
Should we be closing this seeing as @atomgiant 's docs PR has gone in?
There are a few workarounds to access $dispatch in function components and it doesn't sound like we're going to implement this.$dispatch.
:click="submit($dispatch)" and then use the parameter in the JS function: submit($dispatch) { /* use $dispatch */ }document.dispatchEvent(new CustomEvent('event-name'))loving alpine, FWIW, I'm having a similar modal issue... in order to have one modal object at the <body> level, I've implemented something similar to the below -- essentially nesting components:
Inside of the <body>, somewhere, lies:
<a href="..." @click="$dispatch('modal-action', true)">...</a>
And the outer shell is below.
This may not be the right way to do this pattern entirely, but would also be nice to access the <body> element from within the <a> explicitly. I couldn't figure out if this was supported (or encouraged) via the docs.
2c :)
<body
class="{{ body_class }}"
id="body"
x-data="{ 'isDialogOpen': false }"
@keydown.escape="isDialogOpen = false"
x-on:modal-action="isDialogOpen = $event.detail">
<div x-show="isDialogOpen" id="modal" class="modal">
<div
x-show="isDialogOpen"
x-transition:enter="transition ease-out duration-100 transform"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75 transform"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="modal-backdrop">
<div class=""></div>
</div>
<div
class="modal-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="modal-headline">
<!-- modal contents -->
</div>
</div>
<!-- body contents -->
</body>
Thanks again -- this + Tailwind = lifesaver!
@timfee having the x-data on body isn't a great idea
In this case you can make the modal be the component (have x-data on the div) and listen to @modal-action.window
Is there a way to replicate the $dispatch without passing it into the function? I have tried the following:
// This works but can only be caught by a window scoped listener
window.dispatchEvent(new CustomEvent('my-event'))
// This does not bubble for some reason so I can only catch it with a listener on the root element
this.$el.dispatchEvent(new CustomEvent('my-event'))
// Again does not bubble
this.$refs.abc.dispatchEvent(new CustomEvent('my-event'))
EDIT
For anyone else looking, my limited knowledge of javascript and not checking the docs meant I missed the options within the custom event. I assumed that all events would bubble by default, but it seems not. So to replicate $dispatch in your function you need to add the bubbled option.
this.$el.dispatchEvent(new CustomEvent('my-event', {bubbles: true}));
Is the same as (or at least as far as I can see)
<button x-on:click="$dispatch('my-event')"><click/button>
Hey everyone, I'm going to close this issue since it's quite old now and a handful of workarounds have been mentioned in the thread.
For reference, the functionality of $dispatch can be replicated as shown in the comment above, or alternatively you can pass the $dispatch helper through to your helpers using the inline expression.
If anyone feels like this functionality needs to be re-evaluated, please re-open the issue or create a new one and reference this.
Most helpful comment
If you are already in the real javascript part, you can trigger an event in the canonical way. $dispatch is just an utility helper for the html around a standard event dispatching.