When the Virtual DOM is not enough?

296 views
Skip to first unread message

Dan P

unread,
Jun 18, 2016, 6:26:24 PM6/18/16
to Elm Discuss
Hi all, Elm newbie here. I'm trying to emulate the HTML input maxlength attribute when type="number", in order to input an hour of the day. Using vanilla JS I would do the following:

index.html

<html><body>
 
<input id="el" type="number">
</body></html>

main.js

oldvalue = ""

// The value of the element is already set before this function gets called.
document
.getElementById("el").oninput = function (e) {
   
if (e.target.value.length > 2)
     e
.target.value = oldvalue;
   
else
     oldvalue
= e.target.value;
}


However when I write this in Elm using onInput, the user's invalid input is visible for a few milliseconds before being fixed. To me this seems like a vDOM issue, but I really don't know.

Main.elm

model = ""


update
(Input s) model = if (String.length s > 2) then model else s


view model
= [ input [ type "number", onInput Input, value model ] [] ]


I've combed through all the documentation in the Html package, and found nothing that would address this. My idea would be to somehow embed ordinary HTML inside my application (the guide does the inverse of this), but I don't know how to do that either : /


Thanks for your help!

Dan P

unread,
Jun 18, 2016, 10:14:54 PM6/18/16
to Elm Discuss
I have a hack. First I include the following JS code:

var oldval = "";

function validateTime(ev) {
    var s = ev.target.value;

    if (isValid(s))
        oldval = s;                    // Save value and don't modify input
    else {
        ev.target.value = oldval;      // Restore last valid input
        ev.stopImmediatePropagation(); // Listeners (e.g. Elm) ignore bad input
    }
}

Then I added 

attribute "oninput" "validateTime(event)"

to the input element. It's hardly elegant and I'd be glad to know of a better way.

Jason Merrill

unread,
Jun 19, 2016, 9:25:48 AM6/19/16
to Elm Discuss
On Saturday, June 18, 2016 at 6:26:24 PM UTC-4, Dan P wrote:
However when I write this in Elm using onInput, the user's invalid input is visible for a few milliseconds before being fixed. To me this seems like a vDOM issue, but I really don't know.

I think this is closely related to this longstanding issue: https://github.com/evancz/elm-html/issues/51

The problem is that Elm processes events like "input" asynchronously. After the browser fires the input event, some time is allowed to pass before the Elm runtime calls your update and view functions, and then some more time is allowed to pass before your new view is actually rendered to the DOM. Meanwhile, the browser has synchronously changed what is displayed in the input, so if you can't synchronously "undo" that change, the illegal input will be displayed briefly to the user.

I'm not aware of any way to work around this issue within Elm, so long as it continues to processes events like "input" asynchronously.

BTW, if anyone else wants to see this issue for themselves, here's a fully working example you can paste into http://elm-lang.org/try

import Html exposing (Html, Attribute, text, div, input)
import Html.App exposing (beginnerProgram)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String


main
=
  beginnerProgram
{ model = "", view = view, update = update }


-- UPDATE

type
Msg = NewContent String

update
(NewContent content) oldContent =
 
if (String.length content > 2) then oldContent else content


-- VIEW

view content
=
    input
[ type' "number", onInput NewContent, value content ] []


John Mayer

unread,
Jun 19, 2016, 11:37:52 AM6/19/16
to elm-d...@googlegroups.com, b...@gmail.com

Right the problem is that the input isn't sending a keypressed event that we can filter, but is internally updating it's state (opaque to Elm) and sending the new value.

Dan, I think that, architecturally, your solution is a step in the right direction. The validation needs to happen synchronously, internal to the element.

It would be nice to have a first-class way to inject this behavior into the html element. A few ideas:

1) Some sort of annotation to pass a reference to the post-compilation JS identifier as an event attribute.

2) Compile down to a HTML custom element where the validation is internal.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Max Goldstein

unread,
Jun 19, 2016, 11:55:25 AM6/19/16
to Elm Discuss, b...@gmail.com
Is this a problem that React or other vdom-based rendering systems have? If so, what's their workaround? If not, why not?

Jason Merrill

unread,
Jun 19, 2016, 3:36:32 PM6/19/16
to Elm Discuss, b...@gmail.com
I'm not an expert in React, but I don't believe it has this problem (by default). It's totally possible to use virtual dom and still respond synchronously to input events, synchronously rendering DOM updates as necessary.

The issue with Elm is that the runtime allows some time to pass between receiving the input event and calling your model/view update function, and then allows some more time to pass before rendering any updates to the DOM. I gave a few more details in an issue comment: https://github.com/evancz/elm-html/issues/51#issuecomment-226997749

I don't see how it's possible to deal with input elements in a completely correct way if all your logic runs asynchronously, because the browser synchronously changes things in the DOM itself and then just tells you what it did. It'd be better for Elm, I think, if the browser just told you that keystrokes happened and then let you decide when/how/whether to apply them, but that's not the way input elements and their events work today.

On Sun, Jun 19, 2016 at 11:55 AM Max Goldstein <maxgol...@gmail.com> wrote:
Is this a problem that React or other vdom-based rendering systems have? If so, what's their workaround? If not, why not?

--
You received this message because you are subscribed to a topic in the Google Groups "Elm Discuss" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/elm-discuss/Juu6j827wyA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to elm-discuss...@googlegroups.com.

John Mayer

unread,
Jun 19, 2016, 6:22:41 PM6/19/16
to elm-d...@googlegroups.com, b...@gmail.com

> It'd be better for Elm, I think, if the browser just told you that keystrokes happened and then let you decide when/how/whether to apply them, but that's not the way input elements and their events work today.

Putting on my mad scientist hay: One could do that by formatting a normal text element to look like an input, and listen to key events, right?

You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.

Alexey Shamrin

unread,
Jun 19, 2016, 6:30:52 PM6/19/16
to Elm Discuss, b...@gmail.com
Here's how I would implement it in React:

var App = React.createClass({
  getInitialState
: function() {
   
return {value: ''};
 
},
  render
: function() {
   
return <input type="number" onInput={this.handleInput}
              value
={this.state.value} />;
 
},
  handleInput
: function(e) {
   
var v = e.target.value;
   
if (v.length <= 2) {
     
this.setState({value: v});
   
}
 
}
});



It works correctly because <input value=…> is a controlled component: user input
has no effect on the rendered element, until we set the value with setState.

By the way, it is possible to write similar code in Elm:

import Html exposing (Html, Attribute, text, div, input)
import Html.App exposing (beginnerProgram)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput, on)
import String
import Json.Decode as Json


main
=
  beginnerProgram
{ model = "", view = view, update = update }

-- UPDATE

type
Msg
 
= NewContent String

 
| NoOp

update msg oldContent
=
 
case msg of
   
NewContent content ->
      content
   
NoOp ->
      oldContent

-- VIEW

view content
=

  input
[ type' "number", on "input" decodeInput, value content ] []

decodeInput : Json.Decoder Msg
decodeInput =
  Json.at ["target", "value"] Json.string
    |> Json.map (\s -> if String.length s <= 2 then NewContent s else NoOp)

Unfortunately, it doesn't help. Annoying flashing of extra digits is still there.
It seems to me Elm (elm-html?) doesn't support controlled components.

Is there a discussion about adding controlled inputs to elm-html?

Alexey

John Mayer

unread,
Jun 19, 2016, 6:45:55 PM6/19/16
to elm-d...@googlegroups.com

Right, so long as the event needs to be sent "to the top" (it's an old analogy and maybe outdated since Signals went away) this problem will persist. And I think that this is the case so long as we can only attach effect actions to the DOM - basically it's a flaw not only with FRP, but TEA as well.

This kind of stuff needs to be handled out-of-band. We need a way to create a <lengthRestrictedInput> that doesn't send an action to the rest of the app when the invariants don't hold.

Interestingly the behavior that we want to inject into these "input constructors" looks a lot like AFRP.

--

Alexey Shamrin

unread,
Jun 19, 2016, 6:47:33 PM6/19/16
to Elm Discuss, b...@gmail.com
Follow up to my example. Json.Decode is not necessary:

view content =
  input
[ type' "number", onInput handleInput, value content ] []

handleInput : String -> Msg
handleInput s =

    if String.length s <= 2 then NewContent s else NoOp

John Mayer

unread,
Jun 19, 2016, 7:19:03 PM6/19/16
to elm-d...@googlegroups.com, b...@gmail.com

What if:

Html.Events.onInput : (String -> Maybe msg) -> Attribute msg

We could run the validation logic before the DOM event is sent to the global effect system.

--

John Mayer

unread,
Jun 19, 2016, 7:30:30 PM6/19/16
to elm-d...@googlegroups.com

Huh, is this the problem that the NoOp message is supposed to solve?

It's obviously not working.

In cases like this, we need to determine locally that the effect on the app is semantically "no operation", and ignore the event by synchronously rolling back the local state and terminate without propagating.

John Mayer

unread,
Jun 19, 2016, 8:00:59 PM6/19/16
to elm-d...@googlegroups.com

Interestingly, the underlying VirtualDom can prevent a message being sent...

http://package.elm-lang.org/packages/elm-lang/virtual-dom/1.0.2/VirtualDom#on

> If the decoder succeeds, it will produce a message and route it to yourupdate function.

But what happens if the decoder fails? Probably doesn't rollback in the same way as Dan's workaround... Turns out, nothing!

https://github.com/elm-lang/virtual-dom/blob/1.0.2/src/Native/VirtualDom.js#L451

Let's add some logic when the decoder fails to rollback the input to the old value, just like in Dan's workaround. Or maybe be conservative and use a Maybe, or a new type with three options Rollback | NoOp | Message msg.

Anyway I think this solves it :-)

Reply all
Reply to author
Forward
0 new messages