Skip to content

Support two-way-data-binding with custom-input-elements #4838

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

Open
knuspertante opened this issue May 15, 2020 · 29 comments
Open

Support two-way-data-binding with custom-input-elements #4838

knuspertante opened this issue May 15, 2020 · 29 comments
Labels
awaiting submitter needs a reproduction, or clarification custom element

Comments

@knuspertante
Copy link

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

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

@Conduitry Conduitry added awaiting submitter needs a reproduction, or clarification custom element labels May 15, 2020
@Conduitry
Copy link
Member

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.

@Conduitry
Copy link
Member

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?

@knuspertante
Copy link
Author

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?

@brgrz
Copy link

brgrz commented Nov 19, 2020

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}>

@geoffrich
Copy link
Member

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.

@claviska
Copy link

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.

@geoffrich
Copy link
Member

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.

@claviska
Copy link

claviska commented Nov 20, 2020

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.

@brgrz
Copy link

brgrz commented Nov 20, 2020

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?

@claviska
Copy link

claviska commented Nov 20, 2020

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.

@stale
Copy link

stale bot commented Jun 26, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@vultix
Copy link

vultix commented Jul 19, 2021

I like the proposal from @claviska. (Also commenting to prevent staleness)

@sidharthramesh
Copy link

Any progress on this?

@Pugio
Copy link

Pugio commented Nov 11, 2021

I too would love to see this (for Ionic).

@rdytogokev
Copy link

I am interested in this issue as well. I would love two-way binding with custom elements.

@samuelstroschein
Copy link
Contributor

@sidharthramesh @Pugio @rdytogokev please don't spam this thread. Svelte is a community-developed piece of software, that just recently got its first full-time maintainer. Aka instead of spamming, go ahead and make a pull request.

@rdytogokev
Copy link

@samuelstroschein I never meant for my comment to be perceived as spam. In fact, I am shocked that you called what I did "spamming". I would love to make a pull request and help out with this effort. But, I don't know how to help. Or even know where to start. I have spent hours trying to figure out a workaround for this issue on my own. I feel like I am spinning wheels and getting nowhere. While I understand basic JavaScript, I don't view myself as an expert. I consume JavaScript to work on projects, not invent it for platforms. I do appreciate the work you do, in fact, I depend on it. I want to respect you, not bring you down. So where could a user of the project go to have their voice be weighed in for consideration, if not here?

@samuelstroschein
Copy link
Contributor

@rdytogokev Sorry, didn't mean to be offensive. It's just that your comment provides no value which can't be expressed with a simple thumbs-up. I thought it's common etiquette by now. A simple thumbs-up won't notify everyone who is involved and "silently" express interest in (solving) this issue. While commenting "i would love to see this", "i am interested in this too" notifies everyone involved without any value.

But, I don't know how to help. Or even know where to start.

Fair point and same problem I have. But how would I know that from one of X "I am interested" comments? Just comment that directly and for sure someone will help out :)

@MohamedElmdary
Copy link

This library provide a simple solution for this issue might help
https://www.npmjs.com/package/svelte-wc-bind

@Tommertom
Copy link

Tommertom commented Nov 22, 2022

The solution for Ionic (and possibly all wc based UI frameworks) imho calls for svelte wrappers on each element. And these wrappers doing the capture of events to emit new values. Examples are already given here and easy to implement.

While this may seem overkill there are other reasons wrapping web components in Svelte native elements - especially Ionic - type safety, type ahead support and tree shaking. So why not bridging this issue in the same go.

Ionic-svelte package (I am maintainer) in near future will provide these - already present in experimental stage. Easily generated from the Stencil source.

@baseballyama baseballyama added this to the 4.x milestone Feb 26, 2023
@dummdidumm dummdidumm removed this from the 4.x milestone Apr 11, 2023
@karimfromjordan
Copy link

Just to update, Vue handles this with the following option in its Vite plugin. Would be really great if we could do this in Svelte too:

 plugins: [
    vue({
      template: {
        compilerOptions: {
          isCustomElement: tag => tag.startsWith('sl-')
        }
      }
    })
  ],

@CatchABus
Copy link

This feature would definitely be helpful as configurable since there's also the case of svelte-native where devs need a preprocessor to support two-way bindings for mobile input elements.

@Tommertom
Copy link

This feature would definitely be helpful as configurable since there's also the case of svelte-native where devs need a preprocessor to support two-way bindings for mobile input elements.

Do you have link to an example?

@Raphael2b3
Copy link

Raphael2b3 commented Apr 9, 2024

i'd love to contribute to this issue, but i need kind of a documentation that explains the internal processes of svelte in order to find the right place to experiment with. I dont think its really efficient to just read all the source code. But if its the only way to go i ll do it. Help is warmly appreciated.

@ouvreboite
Copy link

ouvreboite commented May 1, 2024

I’m looking into Shoelace currently (especially since FontAwesome´s team took ownership and raised some money for Shoelace v3 WebAwesome) and missing the bind:value on the inputs degrades a bit the experience.

Similarly to @Raphael2b3 I would be interested to (try to) contribute. Should a contribution target Svelte 4 or Svelte 5 ? Is the codebase very different between the two ?

@ouvreboite
Copy link

ouvreboite commented May 2, 2024

In term of feature design, I see (at least) three possibilities:

  • simple: just disable compilation checks for bind: on custom elements (i.e. element whose names contains an hyphen). ➡️ I'll try to implement this solution on the latest Svelte 4 branch.
  • medium: create a new custom-bind: that's basically bind: for custom elements (no property checking). It would make it a bit clearer that we are binding "blindly", and that developpers should be really sure that the property they are binding to exists. I'm not sure what the total amount of work to have it available during autocompletion and so on.
  • hard: add a new "customElementBindings" settings in svelte.config and use that during the bind: compilation checks. Being totally unfamiliar with the codebase I've no idea how complex it would be to dev and test. For example:
customElementBindings: {
  sl-input: ['input'], //this allows to do <sl-input bind:value={email}></sl-input>
  sl-checkbox: ['checked'] //<sl-checkbox bind:checked={acceptTermsAndConditions}></sl-checkbox>
}

One benefit would be that webcomponents libraries could easily provide code configuration snippets to paste in the svelte.config to have strongly typed custom bindings

@dummdidumm
Copy link
Member

I think any solution that just assumes custom elements work like native dom elements for certain bindings is too brittle and paints us framework authors and custom element authors into a corner.

A better approach might be to establish a convention: A binding on a custom element is a combination of a property and an event. i.e. when you do bind:value you expect the property value to be set, and when it changes there's a valuechange event . More generally, bind:<x> means write to property <x> and listen to event <x>change.

@ouvreboite
Copy link

ouvreboite commented May 3, 2024

Thank you for the feedback.

Could we imagine a more specific solution to give control on both the setter and event? (So no need for the custom element authors to strictly follow a convention)

Something like that:
With a exposing a text setter and a onDescriptionChange event that return the text (both are mismatched for the sake of this example). We could have a custom-bind:[setterName]:[eventName]={property}

<script>
  let myDescription;
</script>
<my-custom-element custom-bind:text:onDescriptionChange={myDescription}></my-custom-element>

We could have a shorthand syntax for components that expose an actual property (like it's the case for Shoelace's input.value)

<script>
  let myValue;
</script>
<sl-input custom-bind:value={myDescription}></sl-input>

@AlexRMU
Copy link

AlexRMU commented May 3, 2024

You can create an action that will catch events with a different name and create new ones with the correct.

<script lang="ts">
    import type { Action } from "svelte/action";
    export let text: string;
    type MyChangeEvent = CustomEvent<{ data: string }>;
    const action: Action = (node) => {
        function listener(event: MyChangeEvent) {
            const new_event = new CustomEvent("textchange", { detail: event.detail.data });
            node.dispatchEvent(new_event);
        }
        node.addEventListener("mychangeevent", listener as any);
        return {
            destroy: () => {
                node.removeEventListener("mychangeevent", listener as any);
            },
        };
    };
</script>

<my-custom-element {text} on:mychangeevent={(e) => (text = e.detail.data)}></my-custom-element>
<my-custom-element bind:text use:action></my-custom-element>

You can make it built-in and with the ability to pass the name of the event (if the name differs) and the function (if the path to the value differs).

<my-custom-element bind:text use:custom-change-event={"mychangeevent"}></my-custom-element>
<my-custom-element bind:text use:custom-change-event={(e) => e.detail.data}></my-custom-element>
<my-custom-element bind:text use:custom-change-event={{ name: "mychangeevent", value: (e) => e.detail.data }}
></my-custom-element>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting submitter needs a reproduction, or clarification custom element
Projects
None yet
Development

No branches or pull requests