svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Allow specifying non-special bindings as readonly

Open benjamingwynn opened this issue 1 year ago • 11 comments

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

benjamingwynn avatar Jul 22 '22 14:07 benjamingwynn

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.

Tal500 avatar Jul 26 '22 08:07 Tal500

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}

baseballyama avatar Aug 07 '22 11:08 baseballyama

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.

SystemParadox avatar Aug 07 '22 15:08 SystemParadox

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 avatar Aug 08 '22 03:08 baseballyama

Quite simply, look at 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.

SystemParadox avatar Aug 11 '22 10:08 SystemParadox

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;

adiguba avatar Nov 01 '22 08:11 adiguba

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 avatar Mar 10 '23 06:03 n8allan

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;

n8allan avatar Mar 10 '23 06:03 n8allan

@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.

benjamingwynn avatar Mar 10 '23 09:03 benjamingwynn

@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🙂

Tal500 avatar Mar 10 '23 09:03 Tal500

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

arnard76 avatar Jun 27 '23 01:06 arnard76

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.

hackape avatar Jul 19 '23 17:07 hackape

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.

SystemParadox avatar Jul 19 '23 22:07 SystemParadox

@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.

hackape avatar Jul 20 '23 03:07 hackape

@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...

kenbankspeng avatar Jul 26 '23 07:07 kenbankspeng

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 avatar Jul 26 '23 12:07 hackape

@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.

hackape avatar Jul 26 '23 12:07 hackape

I’ve switched from the shadow variable to a dispatcher event.

On Wed, Jul 26, 2023 at 8:19 AM hackape @.***> wrote:

@kenbankspeng https://github.com/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 https://github.com/sveltejs/svelte/issues/7712#issuecomment-1642470141 .
  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.

— Reply to this email directly, view it on GitHub https://github.com/sveltejs/svelte/issues/7712#issuecomment-1651696609, or unsubscribe https://github.com/notifications/unsubscribe-auth/AGNINCD2AYD6H6OSZS6ICLTXSEDNBANCNFSM54L2DHHQ . You are receiving this because you were mentioned.Message ID: @.***>

kenbankspeng avatar Jul 26 '23 12:07 kenbankspeng