Skip to content
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

Add property pipes/transforms #7265

Closed
brunnerh opened this issue Feb 14, 2022 · 6 comments
Closed

Add property pipes/transforms #7265

brunnerh opened this issue Feb 14, 2022 · 6 comments

Comments

@brunnerh
Copy link
Member

brunnerh commented Feb 14, 2022

Describe the problem

Sometimes a value does not quite fit a property, or the component does not quite fit the value, so the value needs to be pre-processed before passing it in. Likewise, for bind-able properties, the output may not be in the correct format, so some post-processing is required.

This is especially prone to happen with third-party components which cannot be easily changed.

So instead of being able to just use a property or bind directly, one may have to rely on additional events or pass the value through a temporary variable instead.

Describe the proposed solution

To make this easier a pipe/transform syntax could be added to properties, to concisely perform pre-/post-processing in a reusable fashion. I would suggest using the | symbol, similar to how it used for event modifiers e.g.

<script>
  let value;
  const trim = x => x == null ? null : x.trim();
</script>

<input value|trim={value} /> <!-- one-directional, there may not be a sensible shorthand -->
<input bind:value|trim /> <!-- bind shorthand -->
<input bind:value|trim={value} /> <!-- bind longhand -->

This would perform a trimming operation in both directions when used with bind. Alternatively an object could be supplied of the form:

{
  in?: (value: any) => any,
  out?: (value: any) => any,
}

e.g.

<script>
  let value;
  const trim = {
    out: x => x == null ? null : x.trim();
  };
</script>
<input bind:value|trim /> <!-- only trims output -->

in would be called when passing the value into the component/element, out when a value is provided from a property with bind. If a function is not supplied, it acts as the identity function, i.e. the value is just passed through.

When multiple pipes are supplied (| separated, like events), they would be executed left to right on input and right to left on output.


The piping functions could potentially also be provided with the current element/component to make it even more flexible:

type PipeFunction = (value: any, self: HTMLElement | SvelteComponent | undefined) => any;
type Pipe = PipeFunction | { in?: PipeFunction, out?: PipeFunction };

It could probably not be supplied in all cases, like when setting properties on <svelte:options>.

Alternatives considered

Wrapping components is always an alternative, but not a very good one. It produces a lot of overhead and strongly couples the API if all component features should be exposed.

Temporary, intermediary variables tend to have problems with cyclical dependencies, as the updates should run both ways. If a component offers sufficient events this can be circumvented more easily.

I cannot really think of a good alternative but am happy to hear of any, if they exist.

Importance

would make my life easier

@Prinzhorn
Copy link
Contributor

Prinzhorn commented Feb 15, 2022

I think for two way binding this sounds like a useful thing, for simple props this saves exactly one character and I don't see it solving a real problem there:

<input value|trim={value} />

vs

<input value={trim(value)} />

I don't think chaining multiple of these transforms is a good idea. They most likely will always be asymmetric and once you have more than one of them chained it'll take some mental gymnastics to figure out what's happening with your two way binding.

I cannot really think of a good alternative but am happy to hear of any, if they exist.

In a similar scenario I'm using what I call "upgraded" stores. They are like derived stores, but two way. They have an upgrade and downgrade function (similar to your in/out). One example where I use this is with settings. My underlying settings store is serialized to JSON. But in my application I want to use Set for performance reason. So I have an upgraded store that lets me read/write the stored data as Set but it will transparently turn it into something that can be serialized (an array).

let setStore = upgradeStore(
  arrayStore,
  (a) => new Set(a),
  (s) => [...s]
);

I'm sure you can apply something similar to your problem and abstract this nicely into a helper function.

Once again stores save the day 🦸

@brunnerh
Copy link
Member Author

The problem with the store solution is that it is quite verbose and requires the existence another store to begin with. This is an unfortunate tendency that I have observed when it comes to stores: They tend to proliferate.

I would like to avoid introducing stores as much as possible, especially since there currently are some limitations (like using $ syntax for nested stores).

I agree that for just property input this feature is of limited use; my main use cases also revolve around bind. It might still be helpful if access to the component/element is required (second parameter to pipe function) and thus save an additional this binding.

Not being able to chain the pipes directly would not be that big of a deal, they can easily be combined manually in code. E.g.

import { toUpper, trim } from './pipes';
const combined = x => toUpper(trim(x));

(Or higher order functions could be used to combine pipes arbitrarily.)

@Prinzhorn
Copy link
Contributor

Prinzhorn commented Feb 21, 2022

and requires the existence another store to begin with

That's not 100% accurate, as you could two-way sync the upgraded store with the local value variable via a closure + a reactive statement. But yes, this this far from ergonomic.

To me "stores" aren't really stores. They are the glue to make imperative code accessible in a declarative environment. The fact that you can treat $store like a variable and read/write/bind it is insane. That to me is what makes Svelte incredible.

Anyway, I agree that stores are not the solution here. But maybe what you are proposing does not capture the big picture. What I would love to see is being able to hook into reactivity in general. So that I can give regular variables the same treatment I do with stores. I want to hide imperative code behind declarative code. And that is essentially what you are asking for here as well, but only for a limited use-case. I want to do that everywhere. And I think some changes to reactivity are planned in v4 anyway, so let's hope someone comes up with a nice way to do that.

Basically I want something like Proxy for reactivity. That would solve your problem among many others. You could just hook into reading / writing of your value. Now someone go ahead and write a nice rfc for "Reactive proxies" 😄

@brunnerh
Copy link
Member Author

brunnerh commented Mar 2, 2022

Just stumbled upon this issue which seems to be a more specific version of what I wrote here: #3937

Dummdidumm even suggested the same object-based extension:

Thought about the same recently, too, but in a more extendable way: what if the modifier is an object you can provide which has two methods transforming the value on the way in/out?

@coyotte508
Copy link

Using actions is not too verbose:

export function trim(node: HTMLInputElement, cb?: (val: string) => unknown) {
	const updateVal = () => {
		const oldValue = node.value;
		node.value = node.value.trim();

		if (oldValue !== node.value) {
			cb?.(node.value);
		}
	};

	node.addEventListener('blur', updateVal);
	node.addEventListener('keydown', (val) => {
		if (val.key === 'Enter') {
			updateVal();
		}
	});
}

and

<input type="text" bind:value={filename} use:trim={val => filename=val}>

@Rich-Harris
Copy link
Member

Rich-Harris commented Apr 2, 2024

Agree that this isn't desirable or necessary for props (value={trim(value)} is much better than value|trim={value}).

For bindings, this is solvable in Svelte 5 with a getter/setter pair as shown here: #9998 (comment). That issue (#9998) proposes a more general solution to this problem, so I'll close this issue in favour of that one.

@Rich-Harris Rich-Harris closed this as not planned Won't fix, can't repro, duplicate, stale Apr 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants