Svelte: Support two-way-data-binding with custom-input-elements

Created on 15 May 2020  路  10Comments  路  Source: sveltejs/svelte

Custom (Input) Elements like ui5-input uses the propname value for the inputvalue attribute.

But svelte only permit value as propname when the element's name is input, textarea or select

https://github.com/sveltejs/svelte/blob/bdabd89f093afe06d5a4bfb8ee0362b0a9634cfc/src/compiler/compile/nodes/Element.ts#L532

otherwise svelte throws an error that value is not a valid binding-element

pls have a look here:
https://codesandbox.io/s/divine-architecture-3qvlc?file=/App.svelte

custom element pending clarification

Most helpful comment

Just tossing this out here as a possible syntax for fun, so don't take it too seriously. 馃槅

<sl-input name="name" value="foo" bind:value[sl-input]={name} />

<ion-input name="name" value="foo" bind:value[ionInput]={name} />

<fast-text-field name="name" value="name" bind:value[change]={name} />

With something like this, you could infer that it's a custom element by the presence of [eventName] and simply bind the value when a custom event of that name is emitted.

The only thing the custom element needs to commit to is that value will be updated when the event is emitted, which seems totally fair to me.

All 10 comments

The issue here is that Svelte can't tell at compile time which elements might be custom elements, and which might be regular elements. Allowing bind:value on custom elements is essentially asking for the compiler to disable its compile-time checks for what valid bindings are, and also to guess at what the appropriate event to listen to is (in your example, it's input, but it could well also be change), and finally also to assume that the new value is going to be available in target.value.

Thinking back on this, it might be fairly safe to distinguish custom elements depending on whether their name contains a hyphen. So theoretically that takes care of the question of how to handle the validation, but I still don't know what code ought to be generated for this. Do we assume the component class has the private Svelte APIs for handling component binding?

I think a hyphen will be good!

Another idea to distinguish custom elements:
Is it possible to declare custom elements in config?

Like Vue ist does:
https://vuejs.org/v2/api/#ignoredElements

Vue.config.ignoredElements = [/^ui5-/];

But svelte should not ignore this elements completely. This should more a registration of custom elements?

Is it possible to validate the binding with HTMLElement.hasAttribute?

I'm facing this same issue using Shoelace components, for example https://shoelace.style/components/input

fig. 1
<sl-input name="name" type="text" label="Name your tank" bind:value={name}/>

_'value' is not a valid binding on elements svelte(invalid-binding)_

Shoelace does provide a sl-submit event on its <sl-form> custom element which serializes the entire form in event.details (fig. 2) but I'd rather have the ability to directly bind the values of Shoelace input to Svelte variables like in fig. 1.

fig. 2
<sl-form class="form-overview" on:sl-submit={submitNewTank}>

Thinking back on this, it might be fairly safe to distinguish custom elements depending on whether their name contains a hyphen. So theoretically that takes care of the question of how to handle the validation, but I still don't know what code ought to be generated for this. Do we assume the component class has the private Svelte APIs for handling component binding?

I agree that it should be safe to distinguish custom elements based on the presence of a hyphen -- per the spec, custom element names must contain a hyphen, and there are only a few (mostly SVG) native elements that have hyphens in them.

As for what code should be generated, here's what I have to do when using a custom input element that Svelte could potentially compile away (REPL):

<script>
    let inputVal;
    function handleInput(e) {
        inputVal = e.target.value;
    }
</script>

<p>
    Current value: {inputVal}
</p>
<custom-input on:input={handleInput}></custom-input>

It's not a ton of boilerplate, but it can get annoying with multiple custom elements on the page. A binding would simplify this to

<script>
    let inputVal;
</script>

<p>
    Current value: {inputVal}
</p>
<custom-input bind:value={inputVal}></custom-input>

In this example, custom-input is a vanilla custom element, not one created with Svelte. I don't think we should expect the custom element to have the component APIs that come from being generated with Svelte. IMO, the majority of custom elements used in a Svelte app would come from external libraries.

I'm not familiar with the inner workings of the Svelte compiler, but here's a simple approach I think could work:

  1. Allow bind:value on elements containing a hyphen
  2. Automatically attach an input event listener to the element that sets value to e.target.value.

There are some caveats here:

  • For this to work, we'd have to assume that the custom element emits an input event and updates value when it occurs. This is not guaranteed, but we should add documentation outlining the expectation. FWIW, I checked the text input components for two popular custom element libraries (Spectrum and Shoelace), and they both conform to this behavior.
  • Since we can't validate what attributes an external custom element has at compile time, we couldn't emit any warnings if the binding was invalid.
  • This would only work for input value bindings. Other bindings might require a different approach.

For this to work, we'd have to assume that the custom element emits an input event and updates value when it occurs. This is not guaranteed, but we should add documentation outlining the expectation. FWIW, I checked the text input components for two popular custom element libraries (Spectrum and Shoelace), and they both conform to this behavior.

Shoelace components (and really any custom element containing an input inside a shadow root) will emit an input event by _coincidence_ because the native input's event is retargeted to the host element. However, this isn't the event you want to listen for, nor is it guaranteed that input will be emitted by all input-like components. In Shoelace, for example, the correct event to listen for is sl-input.

This is important because sl-input is emitted _by design_, ensuring the correct value is received and parsed from the correct source at the correct time. Contrast this to a retargeted input event, which gets emitted immediately by arbitrary internal elements whenever the user provides input. It's not uncommon for a single custom element to contain multiple inputs that each emit an input event, so this will quickly break two-way binding that uses such a convention.

Example: a custom element date picker that has an internal <select> for the year and multiple <button> elements for month/day controls. Selecting a year would emit input even though the user hasn't necessarily finished providing input. Selecting a day won't emit input because it's just a button. The final value is determined by multiple user interactions, so it's the date picker's job to manage them and decide when an event should be emitted and with what value.

Similarly, not all custom elements make use of value. (Shoelace does by convention, but some libraries and singleton custom elements do not.) So to make this compatible with all custom elements, you'd really need to allow for a customizable prop and event name.

This is important because sl-input is emitted _by design_, ensuring the correct value is received and parsed from the correct source at the correct time. Contrast this to a retargeted input event, which gets emitted immediately by arbitrary internal elements whenever the user provides input. It's not uncommon for a single custom element to contain multiple inputs that each emit an input event, so this will quickly break two-way binding that uses such a convention.

This is a good point -- I didn't think about this. I'm not familiar with enough custom element libraries to know how many emit a custom event for a text input and how many only rely on the native input event -- any idea which approach is more common?

Similarly, not all custom elements make use of value. (Shoelace does by convention, but some libraries and singleton custom elements do not.) So to make this compatible with all custom elements, you'd really need to allow for a customizable prop and event name.

Is the goal to make bind:value compatible with all custom elements? It seems like once you get to the point of needing to customize the event + prop, it's a similar amount of code to wire up the event listener yourself. Also, the syntax bind:value implies that we should be binding to value, imo.

I'm hoping to find a solution that provides a good default case for handling custom elements, but there's always going to be more complex cases that require custom setup.

I'm not familiar with enough custom element libraries to know how many emit a custom event for a text input and how many only rely on the native input event -- any idea which approach is more common?

It's a good practice to use Custom Events to differentiate an event emitted by a custom element, but it's by no means a requirement. Custom Events can even use the same name as a native event, e.g. input or change.

I mostly see Custom Events in the wild, but the naming conventions tend to vary. In Shoelace, you have sl-input. In Ionic, you have ionInput. In Microsoft FAST, you have a Custom Event called simply change. All of these events effectively do the same thing. 馃槙

Is the goal to make bind:value compatible with all custom elements? It seems like once you get to the point of needing to customize the event + prop, it's a similar amount of code to wire up the event listener yourself. Also, the syntax bind:value implies that we should be binding to value, imo.

I agree. I was mostly pointing out that Custom Elements can be (and are) designed in different ways compared to standard form controls. Because of that, it's really not possible to support binding for _all_ custom elements without a more verbose syntax. The question of whether or not it's worth it depends on what that syntax looks like, I guess.

My biggest concern here is settling on "custom elements that utilize a value prop and emit an input event will be capable of two-way binding." As I mentioned above, because of event retargeting, this will cause unexpected behaviors in many custom elements that won't be obvious to the end user.

What's interesting is that we've mostly said goodbye with two-way prop bindings when Angular (in contrast to AngularJS) and Vue were released and did just fine for more than 5 years not having them and having to default to using events from components.

When I saw two-way bindings are back with Svelte I had mixed emotions about it and Svelte tutorial itself warns from overusing it BUT nevertheless I quickly embraced them.

So maybe we're just barking up the wrong tree here?

Just tossing this out here as a possible syntax for fun, so don't take it too seriously. 馃槅

<sl-input name="name" value="foo" bind:value[sl-input]={name} />

<ion-input name="name" value="foo" bind:value[ionInput]={name} />

<fast-text-field name="name" value="name" bind:value[change]={name} />

With something like this, you could infer that it's a custom element by the presence of [eventName] and simply bind the value when a custom event of that name is emitted.

The only thing the custom element needs to commit to is that value will be updated when the event is emitted, which seems totally fair to me.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

robnagler picture robnagler  路  3Comments

ricardobeat picture ricardobeat  路  3Comments

lnryan picture lnryan  路  3Comments

matt3224 picture matt3224  路  3Comments

rob-balfre picture rob-balfre  路  3Comments