Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Event bubbling #149

Closed
punmechanic opened this issue Dec 7, 2016 · 5 comments
Closed

Event bubbling #149

punmechanic opened this issue Dec 7, 2016 · 5 comments

Comments

@punmechanic
Copy link

punmechanic commented Dec 7, 2016

Consider the following components:

Link.html

<button on:click='clicked()'>plz click</button>

<script>
  export default {
    methods: {
      clicked() {
        return this.fire('navigate')
      }
    }
  }
</script>

LandingPage.html

<h1>Hello, world!</h1>

<Link></Link>

<script>
  import Link from '../Link.html'

  export default {
    components: {
      Link
    }
  }
</script>

Router.html

<div ref:container></div>

<script>
  export default {
    onrender() {
      const RouteConstructor = this.get('route')
      const currentComponent = new RouteConstructor({})

      currentComponent.mount(this.refs.container)

      currentComponent.on('navigate', () => {
        console.log('test')
      })

      this.set({
        activeRouteHandler: currentComponent
      })
    },

    onteardown() {
      const activeRouteHandler = this.get('activeRouteHandler')

      if (activeRouteHandler !== undefined) {
        activeRouteHandler.teardown()
      }
    },

    computed: {
      route: routes => routes['/']
    }
  }
</script>

Main.html

<Router routes='{{ routes }}'/>

<script>
  import Router from './Router.html'
  import LandingPage from './scenes/LandingPage.html'

  const routes = {
    '/': LandingPage
  }

  export default {
    data() {
      return {
        routes
      }
    },

    components: {
      Router
    }
  }
</script>

Given the above, I was hoping that Link would propagate its navigate event up to the Router component, which would catch it and then act on that event. Am I doing something FUBAR here or is there an alternative pattern for orthogonal concerns like Routing? I would like to be able to just use a <Link> component without the rest of the application having to be concerned about "how" routing is done and have it "just work".

Currently, clicking the plz click button will not propagate the event up to Router, so nothing happens. The only other alternative I can see is by manipulating the history from the Link component, which sort-of works but I see that being problematic in terms of re-use.

@Rich-Harris
Copy link
Member

Programmatic events don't bubble the way DOM events do, no. I know you're asking for a simple yes/no answer as to whether/how this is possible, but bear with me while I ramble for a bit for the sake of debate and anyone else who is wondering about the rationale for this.

It could be implemented like so:

// in the generated code
component.on( '*', function ( eventName, event ) {
  this.fire( eventName, event );
});

// in the `fire` method
this.fire = function fire ( eventName, data ) {
  var universalHandlers = callbacks['*'];
  for ( var i = 0; i < handlers.length; i += 1 ) {
    universalHandlers[i].call( this, eventName, data );
  }

  var handlers = eventName in callbacks && callbacks[ eventName ].slice();
  if ( !handlers ) return;

  for ( var i = 0; i < handlers.length; i += 1 ) {
    handlers[i].call( this, data );
  }
};

(There's some nuance around callback order etc that I'm glossing over, but you get the idea.)

The trouble is that when you add component event bubbling, it becomes harder to understand how a single component will behave just by looking at it, if it has nested components – you need to be well acquainted with the entire subtree before you can feel confident. Your app as a whole becomes less predictable. (This isn't academic – Ractive, Svelte's forerunner, does allow event bubbling and it's been a source of bugs and confusion on several of my projects.)

Beyond that, if you have event bubbling then you need to have a way of preventing bubbling, analogous to event.stopPropagation(). As well as adding additional logic to this.fire to prevent an event from propagating once it's been stopped, we need some way to stop the event in the first place. Suppose we added a stopPropagation method to the event object – we then have to do that for every single event object that gets generated, or wrap them in special new Event(data) objects with prototypes, as well as keeping track of the event's stopped state. There's a large surface area for weird edge cases.

All that extra runtime code and machinery for a feature that's occasionally convenient, but which is just as often a source of problems!

So I'm inclined not to go down the bubbling route. Which leads to the obvious question: how to make this work?

You could manually propagate events like so:

<Link on:navigate='navigate(event)'></Link>

But that is a little verbose. So I wonder if there's a case for some dedicated syntax for event propagation, which could be as simple as

<Link on:navigate></Link>

(and has precedent in the form of the <Component bind:foo/> short form for bind:foo='foo').

FYI: note that we're discussing removing mount from the public API on #150.

@punmechanic
Copy link
Author

punmechanic commented Dec 7, 2016

Hey, thanks, I figured that it would have something to do with how easy it is to view what a component does - however in some cases, that's desirable in order to encapsulate concerns. For example, if I were to release a routing library, I wouldn't really care about where the Router is coming for my Link component: I just want to use my Link component and have it just "work".

This leads me to one of two options:

  1. Singleton Router in the library using something like Redux - the Router library never exposes a Router instance or concerns about it, it only exposes a Link interface and Router interface. This seems like the best option to take, though it means that my Router now has to include Redux (or something similar, or roll it's own).

  2. Something similar to React's context, but this has the same issues as event propagation.

Thanks for the note on mount. What is the alternative for dynamically mounting components? I only got started on svelte in any capacity last night, so I just picked what looked like it would do the job. It's definitely a naive sledgehammer approach but I didn't see anything in the docs about dynamic insertion of content in this way.

@FWeinb
Copy link
Contributor

FWeinb commented Dec 7, 2016

I have no idea if this is desirable but you can get event bubbeling by using DOM Events. I created a little (very simple) routing example here

The main thing that makes this work is CustomeEvents so the Link-Element is dispatching a navigation event like this:

<a href="#" on:click="navigate(event)" ref:link>{{yield}}</a>

<script>
  export default {
    methods: {
      navigate(event) {
        event.preventDefault();
        this.refs.link.dispatchEvent(
          new CustomEvent('navigate', {
            bubbles: true,
            detail: {
              pathname: this.get('to') // <Link to="/home">Go Home</Link>
            }
          })
        );
      }
    }
  }
</script>

And the Router is using <div on:navigate="updateRoute(event)"></div> to receive this event. And than renders a new component instance each time the route was hit (tearing the old one down)

@Ryuno-Ki
Copy link

@danpantry You closed the issue today. What was your final solution?

@punmechanic
Copy link
Author

punmechanic commented Feb 22, 2017

@Ryuno-Ki I didn't have one. I ended up not using Svelte as it didn't have the tooling support I needed and it was too much effort to get it working compared to something like React, especially since the file size of the application I was working on was not an issue.

I was just going through open issues/PRs/old repo forks and clearing them out.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants