svelte
svelte copied to clipboard
Input modifiers
https://github.com/sveltejs/svelte/pull/3527#issuecomment-534531584
Is your feature request related to a problem? Please describe. Date inputs have string bindings, which are rarely helpful.
Describe the solution you'd like It should be possible to specify the type of binding:
<input type="date" bind:value|date={date_as_date}>
<input type="date" bind:value|number={date_as_number}>
<input type="date" bind:value|string={date_as_string}>
In v4, we can switch the default over to the value|date
behaviour.
Describe alternatives you've considered
Adding valueAsNumber
and valueAsDate
bindings. There are a couple of reasons not to do this:
- It would introduce an inconsistency with how
number
andrange
inputs are currently handled - Resolving that inconsistency would mean expecting people to type
valueAsNumber
instead of justvalue
. It's unergonomic - Having multiple binding options makes it easy for someone to use them all simultaneously, which could lead to subtle bugs from not having a single source of truth
How important is this feature to you? Medium
Might it be nice to add |number
and |string
to type='number'
as well?
Would be good for <select>
which doesn't have a type
attribute.
I still would prefer bind : valueAsDate/valueAsNumber
with the reasoning being: "Don't make me learn how to use another framework" use what the browser provides so that i can apply same knowledge in tomorrows new frameworks
It would just be easier to use the built tools then to write redundant wrappers around something that already exist and can do it for you
I also like bind:value|date
syntax, as I think it's a lot cleaner than the messy API exposed by browsers.
Is there a workaround for now?
@cupcakearmy
<input type="date" bind:value={date}>
$: dateAsDate= date && new Date(...date.split('-'))
@antony Thanks for the answer, but I needed 2 way binding. Got it working though :) https://svelte.dev/repl/dc963bbead384b69aad17824149d6d27?version=3.25.1
Just an idea: Maybe this concept could work in a more general way: I'd imagine having a way to change the internal representation of an input would be very nice. This isn't very well thought out yet, but what about something like
<script>
let date = new Date();
const dateModifier = {
parse: (dateString) => new Date(Date.parse(dateString))
};
</script>
<input type="date" bind:value|dateModifier={ date }>
The nice thing about this would be that you could also use this with other kinds of inputs well. For example, you could automatically have a localized input like this with less code, for example:
<script>
let number = 1234567;
const numberModifier = {
parse: (s) => parseInt(s.replace(/\,/g, "")),
stringify: (n) => n.toLocaleString("en-US")
};
</script>
<input type="text" bind:value|numberModifier={ number }>
...and you would not need the temporary internal variable any more.
I personally find my own code suggestion a bit ugly here, but would love the general idea of being able to have a variable contain something different than the exact input value given by HTML.
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.
Is there any update on this issue? I have run into this problem with <input type="date"/>
multiple times in various apps I have written. What is the recommended workaround if this isn't going to be addressed anytime soon?
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.
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?
Similar to what dummdidumm proposed, I liked the value converter approach Aurelia used for this problem.
export class RgbToHexValueConverter {
toView(rgb) {
return "#" + (
(1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b
).toString(16).slice(1);
}
fromView(hex) {
let exp = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,
result = exp.exec(hex);
return {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
};
}
}
export class Color {
rgb = { r: 146, g: 39, b: 143 };
}
<template>
<require from="./rgb-to-hex"></require>
<label for="color">Select Color:</label>
<input id="color" type="color" value.bind="rgb | rgbToHex">
<br> r: ${rgb.r}, g:${rgb.g}, b:${rgb.b}
</template>
I think this is way to specific and might be missing the big picture, as I've outlined here https://github.com/sveltejs/svelte/issues/7265#issuecomment-1046633268 . The two comments above mine don't go far enough to solve a greater problem.
What I imagine is some sort of Proxy but for reactivity. Stores give us that level of control (hooking into declarative assignments with imperative code), regular reactivity does not.
In the most verbose way this would look like this:
<script>
import proxy from 'svelte/proxy';
import { format } from 'date-fns';
let value = new Date();
$: console.log(value);
// The proxy specification.
const dateTimeAsString = {
get(d) {
return format(d, "yyyy-MM-dd'T'HH:mm");
},
set(s) {
return new Date(s);
},
};
// This reactively marries `value` and `proxiedValue`.
// Everytime `proxiedValue` is written, it is syncted with `value` through `set`.
// Everytime `value` is written, it is syncted with `proxiedValue` through `get`.
// Both `value` and `proxiedValue` can be used just like any variable.
// This gives us the $store declarative magic for any variable.
let proxiedValue = proxy(value, dateTimeAsString);
</script>
<input type="datetime-local" bind:value="{proxiedValue}" />
However, the pipe shorthand allows using a proxy implicitly:
<script>
import { format } from 'date-fns';
let value = new Date();
$: console.log(value);
const dateTimeAsString = {
get(d) {
return format(d, "yyyy-MM-dd'T'HH:mm");
},
set(s) {
return new Date(s);
},
};
</script>
<input type="datetime-local" bind:value|dateTimeAsString />
And of course Svelte comes with a set of build-in proxies:
<script>
import { dateTimeAsString } from 'svelte/proxy';
let value = new Date();
$: console.log(value);
</script>
<input type="datetime-local" bind:value|dateTimeAsString />
Edit: a few words on what I mean by "big picture": This is not limited to bind
at all. You can use it to hook into any reactive variable. In contrast to actual Proxy
this is not limited to objects, because Svelte is a compiler. It just works with every type of variable. If you use an identitiy-proxy ({get(v){return v;}, set(v){return v;}}
) you essentially get an event/hook you can use to imperatively react to the change of a variable or binding. In the same way you can override store.set
simply to do something with the value, e.g. writing it to localStorage
.
I also want to point out that my example above makes it look like proxy
is a function you can use. But no, it's meant to be a compiler instruction so maybe an import is not the right way for that.
I'm sure there is a solution to https://github.com/sveltejs/svelte/issues/4933 and related problems in there as well.
I was playing around with store transforms to achieve a similar result (you can see here: https://svelte.dev/repl/8d60f27d3183493d8ee2264f535167f4?version=3.48.0). I think this is modeling what a lot of people are asking for, but it does require the use of stores. It would be nice to hear what people think.
Something I noticed comes up which may not be immediately obvious, is that not all transformations are symmetrical. That is, not every mapping of A to B has a mapping from B to A. Sometimes a mapping may be ambiguous and sometimes there may just be no mapping.
If the suggestion is to have a fixed list of "out of the box" conversions than this issue may be avoidable, but if the list of conversions is to be extensible it would be worth discussing.
For the library I wrote, I chose to have the read and write transforms offer two forms:
(value: INPUT) => OUTPUT
and (value: INPUT, set: ((value: OUTPUT) => void)) =>void
.
This supports the situation where a specific value may not have a valid conversion. The interface should also be familiar to anyone who is used to the derived
interface.
I'm very interested to hear these conversations so I can inform the design choices in my libraries: npmjs @crikey/*
@antony Thanks for the answer, but I needed 2 way binding. Got it working though :) https://svelte.dev/repl/dc963bbead384b69aad17824149d6d27?version=3.25.1
Thanks for this example! I was exploring which is the most idiomatic way of currently solving this pattern in Svelte (a two way bind using In and Out transformations, I asked about it in stackoverflow) and I had completely missed the simplicity of your approach.
Maybe this is a feature for Svelte 5? Because this missing feature would mean to us, that we need to use handler functions to set a value, not a bind value.
I'm saying it because this ticket is already older and could be forgotten in the backlog
So, I guess my comment above could now literally become $proxy
with Svelte 5? That would solve a whole host of issues and put bind:
(or reactivity in general) on steroids. So pls add $proxy
rune or similar.
See also https://github.com/sveltejs/svelte/issues/7265
Edit: to be clear, $proxy
should not be bind:
specific at all. You should be able to $proxy
any $state
. Somewhat like a two-way $derived
that works with a single dependency.
I agree that $proxy
would be extremely useful.
@Prinzhorn Could you please create an issue specifically for the $proxy
rune and describe your proposal? That would be great.
Strong agree with @aradalvand on https://github.com/sveltejs/svelte/issues/3937#issuecomment-1779002838. 🆙 @Prinzhorn ⬆️
Also, what is the best current minimalistic workaround for this?
I created #9998 for $proxy
.
@cupcakearmy, your workaround https://svelte.dev/repl/dc963bbead384b69aad17824149d6d27?version=4 contains an infinite effect trigger loop bug (stopped before the 2nd iteration of an effect) that still makes one effect to trigger the other. In our case, it cause date input UX bugs
- When a date is set, input 0 with keyboard anywhere in the date input will clear it
- When month and day are set, year input with keyboard can only range from 1901 to 1909
2-Way date binding | valid dates only svelte v4 A fork from @Malix-off fixes 0 input error, by undoing change half-fixes year error. It's not aesthetic but year can be changed. It's fine if user powers through the 4 digits but if they get confused and stop half way then chaos ensues.
It's theoretically fixable by using stores. I'm currently also trying a rune port
@cloudymeatball Your version indeed fixes, in a hacky-way
When a date is set, input 0 with keyboard anywhere in the date input will clear it
but still has
When month and day are set, year input with keyboard can only range from 1901 to 1909
And add some UX bugs when I tried to input some random values in the date input
@cloudymeatball for some reason, on the latest windows and chrome, pressing 4 consecutive digits while having my cursor selection on the input year still only take the last digit in account (except 0, that simply doesn't work), so I can only set 1901-1909
@Malix-off See this breaking https://svelte.dev/repl/f9441e746728408d8ed481e2d3572896?version=4.2.8 which uses the web Date object instead of a third-party library. This fixes the year issue.
@cloudymeatball
@Malix-off See this breaking svelte.dev/repl/f9441e746728408d8ed481e2d3572896?version=4.2.8 which uses the web Date object instead of a third-party library. This fixes the year issue.
It's pretty close, yep!
The input still doesn't act like the native <input type="date"/>
HTML Element in some case, like not allowing the input or tabbing to the next value automatically
@cloudymeatball
@Malix-off In both chromium and firefox, tab, shift+tab, arrows all work normally once the element has focus. We were just adding javascript to a native input. We didn't really take away any browser defaults.
~~Ye manual tabbing and arrow works, but it doesn't switch to the next automatically in some cases on my browser Try "whatever-01-whatever" or "whatever-02-whatever", focus away, focus back to DD, and input 1 or 2 respectively (the same digit that the current last DD). It won't tab automatically to YYYY~~
Never mind, it does the same with the native <input type="date"/>