Skip to content

Allow specifying non-special bindings as readonly #7712

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
benjamingwynn opened this issue Jul 22, 2022 · 23 comments
Open

Allow specifying non-special bindings as readonly #7712

benjamingwynn opened this issue Jul 22, 2022 · 23 comments
Labels
awaiting submitter needs a reproduction, or clarification feature request

Comments

@benjamingwynn
Copy link

benjamingwynn commented Jul 22, 2022

Describe the problem

Currently there appears to be no way to define a components binding as readonly, this is supported on special inbuilt elements, such as input, video and audio (for example, for "read_only_media_attributes") but Svelte currently lacks support for specifying anything outside of inbuilt elements as a readonly binding.

Looking through the source, it looks like the binding type is decided around here, and is conditioned based on the type of element:

https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/Binding.ts#L88

Sorry if this has already been discussed but I couldn't find an existing issue covering this topic.

Describe the proposed solution

We should have some way of specifying a readonly binding, for example, by using the reserved export let $variable syntax:

Foo.svelte:

<script>
  import {onMount} from 'svelte';
  export let $foo = 0;
  
  onMount(() => {
   const t = setTimeout(() => {
     foo++;
   }, 100);
   return (() => {
     clearTimeout(t)
   });
  });
</script>

App.svelte

<script>
  import Foo from './Foo.svelte';
  let foo;
</script>
<Foo bind:foo />
<h1>{foo}</h1>

I'm not aware of if this syntax is intended for something else, someone might be able to chime in and suggest something that might be better suited and fit in better with the general design of Svelte.

Alternatives considered

We'd like this to warn developers not to write to certain values to components as they won't affect anything, since the component that's providing those bindings is doing some side effect that isn't controllable from the parent component.

If they do, it doesn't directly affect any result (nothing happens), so our alternative solution is just to mark these bindings as readonly in our JSDoc.

Importance

nice to have

@Tal500
Copy link
Contributor

Tal500 commented Jul 26, 2022

I also want this feature in Svelte.
Currently, I think that the only "Svelte way" to do this is by firing an event whenever the value changes.

Additionally, there are some generic readonly "props" in Svelte component - those are the slot props.
Maybe it should be possible to bind to slot props? E.g. if Component has an exported slot prop p, this code could be legal:

<Component bind:let:p={var} />

I know that the code isn't the cleanest, but it is just an association.

@baseballyama
Copy link
Member

Why is just passing props not enough?
This prevents to rewrite values of a parent component from a child component.

Foo.svelte

<script>
  import { onMount } from "svelte";
  export let foo = 0;

  onMount(() => {
    const t = setTimeout(() => {
      foo++;
    }, 100);
    return () => {
      clearTimeout(t);
    };
  });
</script>

Foo at Foo.svelte: {foo}

App.svelte

<script>
  import Foo from "./Foo.svelte";
  let foo;
</script>

<Foo foo="{foo}" />
<h1>{foo}</h1>

Foo at App.svelte: {foo}

@SystemParadox
Copy link

SystemParadox commented Aug 7, 2022

This prevents to rewrite values of a parent component from a child component.

This issue is about the opposite, exporting a value from Foo that Foo is in control of, and App can read it but shouldn't be allowed to change it.

@baseballyama
Copy link
Member

Ah got it. Thank you for your explanation!
Could you please explain to us for more concrete use case?

My first impression is that the complexity of the bindings will make them more difficult to understand.

  • parent --> child
  • parent <--> child
  • parent <-- child

Also, at least in my experience, it seems that the timing of passing a child component's value to a parent component is often controlled by the code, rather than when the value is changed. (e.g. passing value to the parent when a button is clicked)
If this is true for most of the Svelte users, I thought we don't need to support it on the Svelte layer.

Thought?

@baseballyama baseballyama added feature request awaiting submitter needs a reproduction, or clarification labels Aug 8, 2022
@SystemParadox
Copy link

SystemParadox commented Aug 11, 2022

Quite simply, look at the special cases svelte has for input, video and audio. The use case is the same. Some components want to expose state back to the parent, but the parent shouldn't be allowed to modify it.

If you don't see the need for this then I would argue the special case should also be removed from the builtin elements. If it can't be removed from the builtin elements then clearly this behaviour is desirable and necessary and we should make it available everywhere.

The issue with timing is actually most of the reason for this request. If you do this with a normal two-way binding then the parent is in charge, which means the initial value comes from the parent, and the only way to get the value from the child is to have the child do something like $: { value = whatever }. Since this doesn't happen immediately you get at least one render with the parent's value, which isn't desirable.

@adiguba
Copy link
Contributor

adiguba commented Nov 1, 2022

Hi,

Nice feature to have, but but I think the $ syntax is confusing with stores, and could cause problems for code analysis on IDEs.

I think it would be clearer to use a specific JS label to declare read-only variables :

export let foo = 0;
export let bar = 0;
export let baz = 0;

// With this syntax, we declare `foo` and `bar` as read-only :
$readonly: foo, bar;

@n8allan
Copy link

n8allan commented Mar 10, 2023

I'm new to Svelte, and was shocked that this isn't already present. Readable stores seems like a way to work around this limitation. For e.g.:

let internalBusyState: Writable<"idle" | "loading" | "saving"> = writable("idle");
export const busyState: Readable<"idle" | "loading" | "saving"> = { subscribe: internalBusyState.subscribe };

Seems inferior to first class read-only properties though.

UPDATE: this doesn't work, though I'm not sure why. On the consuming end of the component the store values just come out undefined. e.g. {dataset.$busyState} is always undefined

@n8allan
Copy link

n8allan commented Mar 10, 2023

I think it would be clearer to use a specific JS label to declare read-only variables :

$readonly: foo, bar;

Would it be more straight forward to have it be a modifier on the property itself? This way the reader immediately knows without looking elsewhere. Something like this:

export readonly let foo = 0;
export let bar = 0;

@benjamingwynn
Copy link
Author

@n8allan as far as i know, svelte JS syntax has to be somewhat ESM compatible, export let bar = 0 is valid ESM, but I don't think export readonly let foo = 0 would be, the only reason the $: syntax works is because Svelte (ab)uses labels.

@Tal500
Copy link
Contributor

Tal500 commented Mar 10, 2023

@n8allan as far as i know, svelte JS syntax has to be somewhat ESM compatible, export let bar = 0 is valid ESM, but I don't think export readonly let foo = 0 would be, the only reason the $: syntax works is because Svelte (ab)uses labels.

We can always have a JSDoc-style comment, though I agree with your suggestion as well.

You suggestion is actually good as we can have that these props will always be reactive🙂

@arnard76
Copy link

This might be a possible workaround using a store:

  1. create a writable store in the child component (e.g. Foo)
  2. export a read-only version of this store (store only has subscribe, no set/update function)
  3. have the parent component (e.g. App) bind a local variable to this read-only store

The writable store value could be changed at any point within the child component so better than using a readable store only.

Seems inferior to first class read-only properties though.

If the parent attempts to change the value of store, svelte generates this error message:
Error: readOnlyStore.set is not a function

Here is a code example:
https://svelte.dev/repl/2a742620fce14ce5bd9f5e27af7825a5?version=4.0.0

@hackape
Copy link
Contributor

hackape commented Jul 19, 2023

This feature can be easily implemented:

<script>
export let value = "foo"
let _value;
// if you want to set init value from outside
// uncomment this line:
// _value = value;
$: value = _value;
</script>

<input bind:value={_value} />

Outside component bind to value cannot affect the real _value, but inner _value change is always synced back to outside through value.

So I don’t think this feature needs language level support. I’d suggest a "won’t do" label for this one.

@SystemParadox
Copy link

Oh that is interesting and useful. Expanding your example a bit, I found a way to make it throw if you try to modify it from outside:

<script>
export let value = "foo"
let _value;
$: value = _value;
function checkValue(v) {
    if (v !== _value) {
        throw new Error('value is readonly');
    }
}
$: checkValue(value)
</script>

So it is doable, but it is very clumsy without language support.

@hackape
Copy link
Contributor

hackape commented Jul 20, 2023

@SystemParadox My impl requires only two extra lines, I think it's pretty far away from clumsy, and to me that already covers all of this feature request. Enlighten me if I missed out something.

I cannot possibly imagine that, if Svelte were to provide language support, it'd implement it the way you describe: to THROW AN ERROR when outside set to readonly prop??? I mean, who want that? Some strange use cases maybe, but definitely not the majority and not the way this feature would be handled.

@kenbanks-peng
Copy link

@hackape Very nice solution with the reactive shadow variable.

With your solution, I suppose the parent code would use the same syntax as a 2-way bind <Child bind:value={parentValue}> and therefore I suggest the shortcoming being that the dev has no compile time / lint feedback within the parent code that changing the bound value does not effect the child value.

@SystemParadox hacked in a runtime feedback for the dev, which I certainly wouldn't want to have in my code, hence the request for language support...

@hackape
Copy link
Contributor

hackape commented Jul 26, 2023

I noticed an earlier remark by @SystemParadox:

The issue with timing is actually most of the reason for this request. If you do this with a normal two-way binding then the parent is in charge, which means the initial value comes from the parent, and the only way to get the value from the child is to have the child do something like $: { value = whatever }. Since this doesn't happen immediately you get at least one render with the parent's value, which isn't desirable.

Just wanna clarify, this is inacurate. If parent value is undefined, then parent value is automatically populated by child value, you don't have to mannually trigger $: { value = whatever } in child. So parent is not always in charge.

@hackape
Copy link
Contributor

hackape commented Jul 26, 2023

@kenbankspeng Yeah if what's important is the "readonly" semantic, it can only be done with language support. Still I don't believe this feature deserves a dedicated language element, for following reasons:

  1. Things like <div bind:clientWidth> as readonly binding is well-known, and is a feature much loved. The fact that "readonly binding exists on some special elements without any clue other than mentioned in the docs" should not surprise any half-serious svelte users.
  2. You can always (and probably should) use custom event to express the "readonly semantic" on component API level, because it really is an event. So there's already an escape hatch. And if you don't care about semantic and only want the readonly runtime behavior, then you can use my workaround.
  3. Simplicity is king. This is the biggest reason why I don't like this proposal. Any new language feature also adds cognitive burden to language user. Before you know, Svelte could also grows into sth with a bunch of features that ppl hate, like every other practical prog-langs. 2nd law of thermodynamics will always find its way, but we can at least try resist it a bit longer.

@kenbanks-peng
Copy link

kenbanks-peng commented Jul 26, 2023 via email

@Ocean-OS
Copy link
Contributor

Ocean-OS commented Jan 3, 2025

Pretty sure this can be closed due to the new bind:prop={null, set} pattern.

@TimoWilhelm
Copy link

Pretty sure this can be closed due to the new bind:prop={null, set} pattern.

But the function bindings syntax is only applicable for the consumer of the binding, right? What I'm looking for is a way to define it on the component side as a readonly binding.

@paoloricciuti
Copy link
Member

But the function bindings syntax is only applicable for the consumer of the binding, right? What I'm looking for is a way to define it on the component side as a readonly binding.

Isn't that just a prop?

@TimoWilhelm
Copy link

Isn't that just a prop?

As mentionend in this thread, there are already readonly bindings in Svelte like dimension bindings: <div bind:clientWidth>.

I would appreciate it if I could define my own bindings and declare them as readonly in the Component I'm defining.

Just as an example I might have a <Header> component that can change it's size dynamically and I need to account for that size in the layout:

let headerHeight = $state(0);
...
<Header bind:height={headerHeight} />

In this case it would never make sense for the wrapper to manually modify the headerHeight variable:

headerHeight = 42; // should throw an error: "headerHeight is readonly"

To me, this is a lot more concise than setting up a onheaderresize callback prop and wiring up the event handlers in the wrapper component.

#7712 (comment) mentions that it would "add cognitive burden" but I would argue that language level support for readonly bindings would actually lessen the cognitive burden by preventing the consumer of a component (that might come from a third-party module) from accidentaly assigning to a binding value that is ment as a readonly binding.

@SystemParadox
Copy link

SystemParadox commented May 2, 2025

Maybe svelte 5.9.0 Function bindings help with this?

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 feature request
Projects
None yet
Development

No branches or pull requests