Open ITenthusiasm opened 2 years ago
If I understand you correctly, your confusion came from thinking that you only have the possibility to do bind:value
for inputs, and that you didn't know that you could use regular input+events combinations? If so, what about the wording in this tutorial step was unclear to you (or in a way so that you didn't think about it later)?
Not quite. I knew about events. However, my confusion was centered on how to format inputs specifically.
So for instance, using a credit card format. (That example uses an action
, though. Alternatives involving state + event handler also exist.) Or perhaps requiring a money format... Basically, something that requires a certain format for the input
, that might automatically format the input, and that prevents users from inputting invalid values.
In other frameworks like React
or Vue
, no explanation for this use case is necessary. Their implementation of one-way data binding (specifically, one-way binding from state
to element
) leaves the state entirely in control and prevents DOM events from updating elements or state on their own. So for formatting inputs, a user would just listen for the DOM event and update the state variable accordingly in a function they made -- as normal.
In Svelte
, DOM events are not prevented from updating the DOM or state variable. Because of this, the order of on:input
and bind:value
matter significantly when it comes to formatting an input (as pointed out in #2446). But this nuance isn't readily apparent anywhere, making formatting a bit difficult for newcomers -- especially from other frameworks.
Does that answer your question?
Maybe we are talking past each other, but what you describe is possible with Svelte, too:
<script>
let value = 'world';
function handle(evt) {
if (value.length > 10) {
value = 'nope';
} else {
value = evt.target.value + 1;
}
}
</script>
<input type="text" {value} on:input={handle} />
You don't need to use bind:value
to make an input accept a value. bind:value
makes this a two way binding. What you want is to split this up into one-way + event. The above code does this and I'm able to adjust the value the way I want, without having to resort to evt.target.value = ..
. That's why I asked if that tutorial chapter wasn't clear about this, but I guess it wasn't. Maybe that tutorial would benefit from having the initial code contain the code to synchronize the input with the variable using on:input
.
Yeah some additional expounding like what you mentioned could be useful.
Regarding your example, my only concern is situations where a person wants to make sure that the value doesn't change. That is, the input is "blocked".
In React
/Vue
, maybe you'd keep the first if
block (or a few). But for the case of the value
not changing, you would just do nothing.
If I understand correctly (maybe I don't), if the developer does nothing in Svelte
, then the value
is automatically set to event.target.value
, right? So doing nothing is not an option. To prevent this default behavior, the developer would basically need a way to assign the value to itself, but that doesn't do anything because the value doesn't change.
I've seen someone do value = String(value)
, but it's very odd to do this, and it wastes space (though not much). And the reasoning will not be readily apparent to a typical developer. (Force-updates are rarely ever apparent in code, as far as I've seen.) Mutating event.target.value
back to value
avoids creating new values unnecessarily. But I suppose someone else could also consider it odd.
@dummdidumm Honestly, the discussion on #6998 and the related Twitter thread make me a little more nervous about this kinda stuff... especially since this topic is even tricky for well-known Svelte
supporters. (When it's that tricky, it's really clear that documentation is important.)
To be fair, the inconsistency that occurs when placing bind:value
beforehand across browsers (in #6998) is worth addressing in its own right as a bug. But that aside, this is all a sign that people need to know the significance of placing a bind:value
after an event handler. And if I understand correctly, the need to mutate event.target.value
back to value
when the desire is to keep the input "frozen"/unchanged for invalid inputs still remains.
The tutorials would greatly benefit from this, since the significance of #2446 is not immediately apparent, nor is it easily searchable in Google -- meaning people will start thinking there are bugs that don't really exist. Input masking/formatting is a perfect candidate to explain this odd scenario, as it's a common use case that already seems to be tripping people up. And it could easily go in one of the sections I previously mentioned. Thoughts?
What's wrong with using the reactive block to correct the value? https://svelte.dev/repl/282c70030a864507852a2769639d4043?version=3.44.2
If you don't want to overwrite the user's value, you can define two variables, e.g., storedValue
and displayedValue
, and use the reactive block to update storedValue
with either valid value of displayedValue
or null
.
@7nik Two Things
1) The main problem is again intuition. Anyone coming from any frontend framework -- even perhaps plain HTML and JS -- would expect all the value
-related changes to happen within the event handler. Reactive blocks are not intuitive at all in this regard. And they split up the sections that a developer has to look across to understand what the code is doing.
2) By the time the reactive block is reached, it may be impossible to attain the original value in order to prevent a change to an input
's value. In other words, the reactive block won't always work. As for managing 2 variables, I tried that once and that isn't very intuitive either.
What confuses and surprises me so much is that in regular HTML + JS, the user would have to mutate event.target.value
. And a similar approach to this is necessary as well if action
s are used. Svelte
, from what I can tell, tries to avoid diverging too much from plain HTML + JS. In that case, it makes the most sense to either use actions or take the approach I recommended: adjusting event.target.value
.
I may also have to clarify the problem statement. I tried to keep it shorter in order to reduce the amount of reading people have to do. But historically on the Discord, I've had to explain more specific details as the conversation has gone on. But no one has provided a greater solution for this use case than mutating event.target.value
from what I've seen so far. Kev suggested action
s, which was really good. But as far as clear docs/tutorials are concerned, I think people will need something lower level.
The binding is just a way to synchronize a variable and an element's property. It isn't designed for wedging in a logic "to format the input or something". Assigning to element.value
doesn't produce any events, so no one will know that you've changed it until you dispatch an event. Thus you get weird and buggy behavior.
If you need more than just synchronize a variable and a property - don't use binding, do it yourself with whatever additional logic you need. And then wrap all the logic into a separate component so you'll have something like <CardNumber bind:value />
where value
will contain either a valid card number or nothing.
Another way I see is using action
to validate and synchronize the field, but with storage passed as a parameter to the action.
However, it'd be nice if bind:
could accept additional parameters, e.g., data validation and conversion functions. Because recently, I have to deal with the datatime field and it returns a string while I want to work with a Date object. So I had to have two variables: one for string value and another for date object and use reactive blocks to synchronize them.
The binding is just a way to synchronize a variable and an element's property. It isn't designed for wedging in a logic "to format the input or something".
I'm not sure this is 100% true. But it's fair to say that people shouldn't need to create state variables [that they aren't using elsewhere/otherwise] every time they want to format an input [in the context of a general page or section of a page].
Even so, the issue isn't just trying to approach formatting strictly by using state variables. The issue is when an input needs to be formatted and the value needs to be synchronized and the input needs to remain "unchanged" when something invalid was entered (whether "unchanged" means literally unchanged, as above, or it means the value was coerced via regex, etc. to a new value that we're hoping the regex, etc. properly determined to be the previous state of the input).
To be fair, even outside that use case, it's impossible to [easily] prevent an input from changing if the correct format is not adhered to because an event handler has no knowledge of what the previous state of the input was by itself. Things like regex are an option, but you have to be incredibly clever and specific with how you employ it as the use case gets more complex (like with money, which seems harder than card numbers). And even then, it's possible to run into bugs. With state variables, you're guaranteed that the input's value did not change at all, and with minimal effort.
And then wrap all the logic into a separate component
I'd rather avoid using components because it complicates/restricts styling due to scoping. An action
(as was mentioned) would play more nicely I think.
Assigning to element.value doesn't produce any events, so no one will know that you've changed it until you dispatch an event. Thus you get weird and buggy behavior.
From what I understand, the behavior I suggested isn't buggy at all. An event has already been produced in the scenario I gave, and that event is exactly what will cause the element to update. When bind
is used, whatever comes out of event.target.value
(from the event handler) will always update the state variable if the state variable itself wasn't already updated.
Another way I see is using action to validate and synchronize the field, but with storage passed as a parameter to the action.
Yes. Dig actions. I'm using them in my app. And from what I can tell, you don't need to pass in the second parameter for storage if you order directives properly (#2446). They're great for re-use, and they don't create the component issue I mentioned earlier. The problem again comes back to intuition, and how easily a user would find the help in a tutorial. I spent a really long time trying to figure out how to format money inputs (inputs in generally, really). I did see a public component. But personally it seems too convoluted. The main point of this issue is that people need a clear way to do validation + formatting.
Though, if action
s seem like a better place to put the approach, that's also fine.
Mkay. Circling back around to this after exploring a few options in my own projects. I think this is still an option for sure... But I probably disagree with it now. There is a different "purist" option that can be used instead... though the easiest way to make it re-usable is yet again through actions. (However, someone could just as will put this in the <script>
tag without the wrapping action.)
// formatAction.ts
function formatAction(input: HTMLInputElement) {
let lastValidValue: string;
function handleBeforeInput(event: InputEvent & { target: HTMLInputElement }) {
lastValidValue = event.target.value;
}
function handleInput(event: InputEvent & { target: HTMLInputElement }) {
const { value, selectionStart } = event.target;
if (/* Value does not match required formatting */) {
event.target.value = lastValidValue;
const cursorPlace = selectionStart - (value.length - event.target.value.length);
requestAnimationFrame(() => event.target.setSelectionRange(cursorPlace, cursorPlace));
return;
}
lastValidValue = value;
}
input.addEventListener("beforeinput", handleBeforeInput as EventListener);
input.addEventListener("input", handleInput as EventListener);
return {
destroy() {
input.removeEventListener("beforeinput", handleBeforeInput as EventListener);
input.removeEventListener("input", handleInput as EventListener);
}
};
}
<!-- Somewhere in a Svelte file -->
<input type="text" use:formatAction />
Surely there had to have been a way to format (and restrict) input
s before frontend frameworks started popping off. And I imagine they looked something like this.
This approach seems a bit more verbose? But I'm assuming (?) that since it doesn't involve state
, this won't cause any unnecessary re-renders. Additionally, as @7nik was hinting at, it's more re-usable because it doesn't depend on state. This can be slapped directly into any input
while allowing the consumer to customize the HTML element. Or both of the handlers can be placed on a single input
element while the lastValidValue
is managed internally in a <script>
tag without ever being exposed to the template
.
Maybe some explanation on formatting would be better handled in the action
s section after all? But I'm also wondering if it's just a matter of knowing pure HTML and JS better at this point, since it could be done without state. (Which would hopefully improve performance, re-usability, and readability?)
I think it easily gets missed, so I just want to add the reminder that the key struggle here is how to prevent invalid inputs from the user. Altering inputs is very easy. Prevention (which has valid uses cases across several websites) requires more effort and can't be accomplished through this method.
Some mutation of event.target.value
will inevitably be necessary for this use case. But it's very straightforward and isn't really a problem.
Using the beforeinput
event can be an excellent way to validate and format inputs. The only headache is to get the next value.
Here is what I got: https://svelte.dev/repl/f45d38ec017749ddbcb86310d1102f82?version=3.46.4
Describe the problem
It's not readily apparent from the docs how to format
input
s (at least not for beginners). This can be a significant source of frustration/confusion. I think this is especially true for devs coming from other frameworks, where people are used to easily forcing inputs to adhere to state variables.I've spent a good few hours between days trying to figure out how to make a money-formatted
input
element inSvelte
. I almost opened a new issue in ignorance asking for one-way data-binding fromstate
toelement
(like in React forinput
s) until I saw #6197, and then saw Rich mention masking in #2446.Describe the proposed solution
I believe the
Tutorials
would benefit greatly from a brief explanation on this. Intuitively, when I was trying to figure out how to format myinput
s, I searched the part of the tutorials that talked about inputs. Perhaps a new section could be added calledFormatted Inputs
orMasked Inputs
. I know this is under thebinding
s section, but since it relates tobinding
and to the order of directives, I think this is a proper location.Whatever comments are made, the markup would probably include something like
with the script being something like
Alternatives considered
Alternatively, something like
action
s could be used in the tutorial... which might influence the location of this section. The important thing to me would be whatever makes things clearer/easier. I would think that the previous approach would require less of an established foundation for beginners.This information could also be placed somewhere outside of the docs. But it would be great to have some kind of clear base example easily accessible for all people.
Importance
would make my life easier