alpinejs / alpine

A rugged, minimal framework for composing JavaScript behavior in your markup.
https://alpinejs.dev
MIT License
27.98k stars 1.23k forks source link

Expose Magic Properties to function component #225

Closed TomS- closed 4 years ago

TomS- commented 4 years ago

I have some logic that runs and is complicated as it checks a url against a regex and various other things. After that I want to dispatch an event so I can capture it on the parent component. Unfortuntely when using the $dispatch magic property using this.$dispatch within the function I get the error this.$dispatch is not a function

I checked this and I was getting the proxy as expected. It would be good if we could use the magic properties within the functions too.

EDIT: Example

init() {
            var flkty = new Flickity(this.$el, {
                wrapAround: true,
                prevNextButtons: false,
                pageDots: false
            });

            flkty.on('staticClick', (ev, p, el) => {
                let anchor = el.querySelector('a');

                if(anchor) {
                    anchor.onclick = (ev) => {
                        let href = anchor.getAttribute('href');
                        let [result, provider, id] = href.match(/(youtube|vimeo)\.com\/(?:watch\?v=)*([0-9\w]*)/);

                        if(provider) {
                            ev.preventDefault();
                            this.$dispatch('open-modal', { provider: provider, id: id });
                        }
                    };
                }
            });
        }
HugoDF commented 4 years ago

@TomS- In this case you can pass $dispatch and $el into x-init (x-init="init($dispatch, $el)") and even add it to this. Magic properties are available on the instance for other methods, just not the x-init handler.

<div x-data="page()">
  <div @custom="handleCustom($event)">
    <div x-data="nested()" x-init="init($dispatch, $el)">
      This is a nested component that dispatches an event of type "custom" on x-init
    </div>
  </div>
  <label>Output in parent component: "<span x-text="output"></span>"</label>
</div>
<script>
function nested() {
  return {
    init($dispatch, $el) {
      $el.innerText = "Edited by x-init handler: " + $el.innerText;
      $dispatch("custom", {
        some: "data",
        more: "data"
      });
    }
  };
}
function page() {
  return {
    output: "",
    handleCustom(event) {
      this.output = `Received Custom event "${
        event.type
      }" with payload  ${JSON.stringify(event.detail)}`;
    }
  };
}
</script>

See a full codepen at: https://codepen.io/hugodf/pen/WNvEMLL

calebporzio commented 4 years ago

I agree that $dispatch() should be callable like this.$dispatch() in a function.

Currently, $dispatch() uses the element the expression is registered on the dispatch the event from.

I imagine a good default for this.$dispatch() would be to use the root element of the Alpine component to do the dispatching?

Thoughts?

HugoDF commented 4 years ago

@calebporzio the magic properties (like $dispatch) are callable from functions, I think it's just for x-init that they're not on "this"

SimoTod commented 4 years ago

Hi @HugoDF I think $dispatch is available when used inside a directive since it's added by saferEval and saferEvalReturn but if you use it in a function as Tom said, it does not work.

For example

<div x-data="controller()" x-on:custom-event="foo = $event.detail.newValue">
  <span x-text="foo"></span>
  <button x-on:click="test()">Turn 'bar' to 'baz'</button>
</div>

<script type="text/javascript">
  function controller() {
    return {
      foo: 'bar',
      test: function() {
        this.$dispatch('custom-event', {newValue: 'baz'})
      }
  }
}
</script>

However, it's possible to pass $dispatch in any directives as you said, which feels correct since it preserves the source element.

<div x-data="controller()" x-on:custom-event="foo = $event.detail.newValue">
  <span x-text="foo"></span>
  <button x-on:click="test($dispatch)">Turn 'bar' to 'baz'</button>
</div>

<script type="text/javascript">
  function controller() {
    return {
      foo: 'bar',
      test: function($dispatch) {
        $dispatch('custom-event', {newValue: 'baz'})
      }
  }
}
</script>

@calebporzio and others. I'm a bit torn about this.$dispatch. If we add it, we should make clear in the documentation that the event will be triggered on the root element of that component because there will be edge cases where it won't be so obvious. For instance, this snippet would not update foo since the event is triggered on <div x-data="controller()"> despite the function being called from the button.

<div x-data="controller()">
  <div x-on:custom-event="foo = $event.detail.newValue">
    <span x-text="foo"></span>
    <button x-on:click="test()">Turn 'bar' to 'baz'</button>
  </div>
</div>

<script type="text/javascript">
  function controller() {
    return {
      foo: 'bar',
      test: function() {
        this.$dispatch('custom-event', {newValue: 'baz'})
      }
  }
}
</script>
HugoDF commented 4 years ago

The way I see it, it's something that will be difficult to remove later and adds edge cases.

The solution IMO is to document & educate how to pass $dispatch around instead of adding to "this".

TomS- commented 4 years ago

@HugoDF I'm absolutely fine with passing it through to the function, even other magic properties such as $event. It makes sense to educate people as it seems these properties are not available making functions less functional. I think this is a documentation thing more than a change to core.

HugoDF commented 4 years ago

Closing this in favour of https://github.com/alpinejs/alpine/issues/143 (which covers the same topics I believe).

Feel free to reopen or start a discussion