We've been using Svelte in our production applications for about 3 months now. For the most part, it delivers on the promise to reducing framework nonsense and we absolutely love it.
However, one thing that continues to be burdensome is Svelte's custom event handling. In hundreds of cases we have, we'd like to use DOM events over Svelte events, and we certainly don't want to proxy all of that. So, unfortunately we've adopted a practice that requires all our components to have a root element that is exposed to consumers. This allows us to use all the standard DOM apis on our Svelte components which is particularly useful for event handling. It's also a model that is more consistent with web components custom elements.
As much as we love Svelte, we (and you, I presume) are looking to the future when web components can be used natively with full support. It would be great if there were a way to get one step closer by getting away from a custom event system. Food for thought, keep up the great work.
In hundreds of cases we have, we'd like to use DOM events over Svelte events
Why?
Why?
Mainly because we don't want to proxy all of this:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
In our case we are not only developing applications in Svelte, but also a component library of "primitives" (form elements, other native replacements, etc.). Other devs in our company are target consumers of this component library, even in non-Svelte environments, and it is easier if we can mirror existing html paradigms and capabilities. We've run into many cases where there was simple confusion like:
"why can't I bind to input or keydown?"
"nothing happens when I set offsetTop"
"autofocus isn't working"
Consumers are expecting the same capabilities that the DOM provides and when it is removed it becomes both an education challenge as well as a problem working around the removed limitations. At the end of the day we provide access to a root element so devs can get around any limitations in the component api itself. We felt it was too burdensome to proxy all possible DOM apis but we still do proxy the most common cases.
Polymer has a better model in this regard, by making the DOM node the component itself. Ideally Svelte could do something similar where we don't have to give up the DOM in exchange for a framework. Is there a more elegant way to have both than providing access to a root node?
Svelte doesn't put any wrappers over events, the on:event attribute directly adds an event handler to that event name. I don't think there's such a thing as "Svelte events".
As for setting specific values to the element, you're right that Svelte doesn't support everything and that Svelte doesn't support binding to every value an element can have.
For exposing the root node, wouldn't something like this work?:
<div class="root" ref:root></div>
<script>
export default {
oncreate() {
this.set({ root: this.refs.root })
}
}
<script>
Then simply call component.get('root') elsewhere in your code? I can't really say needing a component's root node in other files is a huge use case that users need..
@PaulBGD
Here is what I mean by proxying events through Svelte:
<!-- MyButton.html -->
<button
on:blur="fire('blur', event)"
on:click="fire('click', event)"
on:focus="fire('focus', event)"
on:keyup="fire('keyup', event)"
on:keydown="fire('keydown', event)"
on:keypress="fire('keypress', event)"
>{{yield}}</button>
<MyButton on:focus="onFocus(event)">Click Me</MyButton>
Svelte isn't wrapping events, we are because we need access to them from the context of a Svelte component.
Then simply call component.get('root') elsewhere in your code?
Yes, that is essentially what we are doing to mitigate the above problem:
<MyButton ref:button>Click Me</MyButton>
<script>
...
const button = this.refs.button.get('root');
button.addEventListener('focus', this.onFocus);
...
</script>
I am open to more elegant ways to handle this.
Honestly seems like you might need to adopt an MVC or MVP pattern in your code, because there isn't really a great way for Svelte as a framework to get around passing events to a parent or an easier way to get a reference to a DOM node.
If you have any suggestions on how to reduce some of the code you're writing, I'd like to hear them. I just personally don't see how much more Svelte can do.
This is possible, but it would require breaking changes.
If Svelte added a backwards-incompatible restriction on components, enforcing a single root element, then it could ditch its custom eventing and use the native DOM eventing. It could even allow attributes to be set from the outside (presumably overriding existing ones within the template definition) and differentiate between attributes and binders with colon prefixing like this:
<MyButton
class="blue"
:toggle="true"
on:click="setState({ isToggled: this.value })"
>Click Me!</MyButton>
What it comes down to is whether this compliments the vision of where Svelte is going. I personally am a fan of keeping it closer to native HTML and love this proposal. Lots.
@PaulBGD
If you have any suggestions on how to reduce some of the code you're writing, I'd like to hear them. I just personally don't see how much more Svelte can do.
One solution would simply be to have svelte create a wrapping DOM element. This guarantees there is always a root, and consumers can treat markup components as elements (depending on the implementation, they might actually be elements).
Thanks for the thoughtful discussion. I'm wary of introducing a constraint that a component should have a single root node (even React is finally abandoning that limitation!), or more generally that components should strongly 'resemble' DOM nodes at all — I think it could quickly become overly restrictive in ways that are hard to anticipate.
For example, one of the bits of feedback:
"nothing happens when I set offsetTop"
For that to change, either components would literally have to become web components, or they would have to implement all the getters/setters you get on DOM nodes:
// using scrollTop rather than offsetTop, as offsetTop is read-only
Object.defineProperty(componentInstance, 'scrollTop', {
get () {
return this.rootNode.scrollTop;
},
set (val) {
this.rootNode.scrollTop = val;
}
});
Neither option is realistic, so rather than straying into the uncanny valley I think it's far preferable to have clear blue water between DOM nodes and components, and avoid creating the expectation that you can treat one like the other.
So for me, the question becomes more about identifying the sources of friction and seeing if there are ways to reduce it without buying into the web component model. For example, maybe we could create a more convenient syntax for propagating DOM events:
<!-- this... -->
<button on:blur on:click on:focus on:keyup on:keydown on:keypress>{{yield}}</button>
<!-- ...could be equivalent to this -->
<button
on:blur="fire('blur', event)"
on:click="fire('click', event)"
on:focus="fire('focus', event)"
on:keyup="fire('keyup', event)"
on:keydown="fire('keydown', event)"
on:keypress="fire('keypress', event)"
>{{yield}}</button>
Also, given that this is a situation where it sounds like you're using Svelte components in the same way that you would use web components (i.e. as leaf nodes in your app), I would be interested to know if svelte-custom-elements might actually be a better fit.
For example you could theoretically do something like this...
<my-button on:focus='onFocus(event)'>click me!</my-button>
<script>
import './components/my-button.js';
export default {
onFocus (event) {
// `event` is the original DOM event, not a Svelte event
}
};
</script>
...where components/my-button.js looks like this:
// this file could be imported once at the top of your app
// and it would be available everywhere, meaning you
// wouldn't even need to import it in individual components.
// you could even replace it with a non-Svelte implementation
// without affecting any other part of your app, if you were
// so inclined
import 'probably-need-some-web-component-polyfills';
import MyButton from './MyButton.html';
import { register } from 'svelte-custom-elements';
register('my-button', MyButton);
I say 'theoretically' because there may be some nuance around e.g. needing to extend from HTMLButtonElement instead of HTMLElement that svelte-custom-elements currently fails to capture.
Once again, I totally agree with @Rich-Harris
I may be reading this incorrectly, but it appears to me there are multiple dimensions to this, some of which are partially analogous to event retargeting from Shadow DOM, and some of which are only theoretically satisfied by Custom Elements v1, even aside from svelte-custom-elements.
Button is a great example. That essential event, focus, doesn't bubble. And buttons "click" on interactions some other focusable elements don't, e.g. spacebar press on some systems.
@Rich-Harris's approach seems right according to the standards and if what you're after is a custom button component that behaves in those ways just like a "native" button, and you can actually extend HTMLButtonElement in the browser and the browser does behave afterward the way it's supposed to. Although, practically speaking, that part of custom elements is less well supported than the rest, see Chrome's status on customized built-in elements (not to mention the ongoing debate in https://github.com/w3c/webcomponents/issues/509). Unfortunately, when I experimented with it, extending the built-in element didn't work so well for elements that are difficult to style appropriately like, say, select where you may be using a technique like encapsulating a redrawing of the element in addition to the actual element in your component to preserve form behavior. FYI, though, I haven't revisited it in a while.
Something else nagged at me as well:
<my-button>
<button>My Text</button>
</my-button>
If my-button extends HTMLButtonElement and that works the way it's supposed to, e.g. my-button becomes focusable and clickable in its own right, as I tab through and hit enter, how does the Svelte component inside receive a focus event or click from my-button? In @alindsay55661's example this didn't come up, but I've had cause to do something internally in a component on a common interaction event while also needing users to be able to execute their own functionality. For a button related example, it might show a "busy" icon or shift to a "clicked" state (though there are alternative approaches for those purposes which have additional benefits, I'm pretty sure there are better examples I can't think of ATM). It also seems like it'd be troublesome in a slightly different way even if you take the inner button out, because the Svelte component really shouldn't be reaching up to its parent to add event listeners or apply style (like resets, button themes). Maybe there's a capturing phase workaround or I misunderstand svelte-custom-element? Mind you I haven't actually tried it yet, maybe I should've waited to post until I had.
What about when the component is more complex than that? A web component doesn't need to have a single root element in its contents (I think strictly speaking it does have one, the shadow root itself, but for the analogy and purpose of this it's not relevant), and I found that restriction irritating in React. In a Shadow DOM the elements inside the component are supposed to be encapsulated, so events are retargeted such that a user can put an event listener on the element hosting the Shadow DOM in the user's light DOM and the event bubbling from inside will appear to come from the host.
I'm going to lobb only one grenade into the midst of all this. In general, I agree w/ @Rich-Harris about avoiding complexity and just trying to answer the basic need, but I've also found myself wishing I could reach for access to components all over the DOM (so to speak), so I agree with the spirit of this sentence (not necessarily the law) from @alindsay55661:
Polymer has a better model in this regard, by making the DOM node the component itself.
I can't tell you how many times I've wanted to reach out and grab a component to do something in another component, and there's no API to do this sort of (imaginary) thing:
SvelteApp.querySelector('SomeComponent')
or
SomeComponent.querySelector('SomeComponent_by_name_or_id_or_some_other_handler')
This is where people usually make the case that such an API creates spaghetti code or isn't true to the spirit of encapsulation or whatever. But I think the events example cited above is an exact case of why the DOM allows random selection through a JS querySelector API. I also agree though that we shouldn't mix and match DOM and Svelte. But there ought to be a similar API built into Svelte. If this is the wrong place to voice that opinion, I'll happily move the discussion elsewhere. :)
Maybe it would make sense to mark one of the dom nodes in the template as a root node so that you could automatically proxy necessary events without typing too much? This way there is no reason to ensure there is a single top node. This would require some more complex static analysis but could be a fair compromise. Less repetition would be handy too.
Example:
<button is:root><slot></slot></button>
Usage:
<Button on:click='foo(event)'>bar</Button>
I feel far less strongly about my comment above now that Svelte has Store, however, I still think there's room for discussion about a way to treat Svelte as a universe of independent components that can work together—spaghetti or not—interoperable, much like the DOM behaves in the JavaScript world.
Just going through old issues to see if there are any that can be closed. This discussion predates Svelte's built-in support for custom elements, which — while improvable — probably serves the needs described here better than any new features could. So I think we should close this issue and focus on adding whatever is missing from our custom element story.
Most helpful comment
Mainly because we don't want to proxy all of this:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement
https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
In our case we are not only developing applications in Svelte, but also a component library of "primitives" (form elements, other native replacements, etc.). Other devs in our company are target consumers of this component library, even in non-Svelte environments, and it is easier if we can mirror existing html paradigms and capabilities. We've run into many cases where there was simple confusion like:
"why can't I bind to input or keydown?"
"nothing happens when I set offsetTop"
"autofocus isn't working"
Consumers are expecting the same capabilities that the DOM provides and when it is removed it becomes both an education challenge as well as a problem working around the removed limitations. At the end of the day we provide access to a root element so devs can get around any limitations in the component api itself. We felt it was too burdensome to proxy all possible DOM apis but we still do proxy the most common cases.
Polymer has a better model in this regard, by making the DOM node the component itself. Ideally Svelte could do something similar where we don't have to give up the DOM in exchange for a framework. Is there a more elegant way to have both than providing access to a root node?