svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Input modifiers

Open Rich-Harris opened this issue 5 years ago • 49 comments

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 and range inputs are currently handled
  • Resolving that inconsistency would mean expecting people to type valueAsNumber instead of just value. 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

Rich-Harris avatar Nov 16 '19 17:11 Rich-Harris

Might it be nice to add |number and |string to type='number' as well?

Conduitry avatar Nov 16 '19 19:11 Conduitry

Would be good for <select> which doesn't have a type attribute.

Deskbot avatar Nov 17 '19 22:11 Deskbot

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

jimmywarting avatar Nov 27 '19 20:11 jimmywarting

I also like bind:value|date syntax, as I think it's a lot cleaner than the messy API exposed by browsers.

antony avatar Sep 16 '20 11:09 antony

Is there a workaround for now?

cupcakearmy avatar Sep 18 '20 22:09 cupcakearmy

@cupcakearmy

<input type="date" bind:value={date}>

$: dateAsDate= date && new Date(...date.split('-'))

Rendered

antony avatar Sep 18 '20 22:09 antony

@antony Thanks for the answer, but I needed 2 way binding. Got it working though :) https://svelte.dev/repl/dc963bbead384b69aad17824149d6d27?version=3.25.1

cupcakearmy avatar Sep 18 '20 23:09 cupcakearmy

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.

pal03377 avatar Feb 20 '21 21:02 pal03377

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.

stale[bot] avatar Jun 26 '21 20:06 stale[bot]

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?

CindyKee avatar Jun 27 '21 03:06 CindyKee

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.

stale[bot] avatar Dec 24 '21 04:12 stale[bot]

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?

dummdidumm avatar Dec 24 '21 08:12 dummdidumm

Similar to what dummdidumm proposed, I liked the value converter approach Aurelia used for this problem.

E.g.:

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>

harvey-k avatar Feb 03 '22 01:02 harvey-k

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.

Prinzhorn avatar Mar 03 '22 10:03 Prinzhorn

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/*

WHenderson avatar May 12 '22 13:05 WHenderson

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

jsilveira avatar May 28 '22 19:05 jsilveira

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

uvulpos avatar Jun 16 '23 12:06 uvulpos

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.

Prinzhorn avatar Sep 21 '23 06:09 Prinzhorn

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.

aradalvand avatar Oct 25 '23 10:10 aradalvand

Strong agree with @aradalvand on https://github.com/sveltejs/svelte/issues/3937#issuecomment-1779002838. 🆙 @Prinzhorn ⬆️

Malix-Labs avatar Dec 24 '23 16:12 Malix-Labs

Also, what is the best current minimalistic workaround for this?

Malix-Labs avatar Dec 24 '23 16:12 Malix-Labs

I created #9998 for $proxy.

aradalvand avatar Dec 25 '23 07:12 aradalvand

@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

  1. When a date is set, input 0 with keyboard anywhere in the date input will clear it
  2. When month and day are set, year input with keyboard can only range from 1901 to 1909

REPL about a workaround fixing the 2nd UX bug

Malix-Labs avatar Dec 27 '23 18:12 Malix-Labs

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.

cloudymeatball avatar Dec 27 '23 20:12 cloudymeatball

It's theoretically fixable by using stores. I'm currently also trying a rune port

Malix-Labs avatar Dec 27 '23 20:12 Malix-Labs

@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

Malix-Labs avatar Dec 27 '23 21:12 Malix-Labs

@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-Labs avatar Dec 27 '23 22:12 Malix-Labs

@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 avatar Dec 27 '23 22:12 cloudymeatball

@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

Malix-Labs avatar Dec 28 '23 00:12 Malix-Labs

@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"/>

Malix-Labs avatar Dec 28 '23 01:12 Malix-Labs