virtual-dom: Event onWithOptions doesn't allow selective behaviour on events

Sometimes its necessary to stopPropogation and preventDefault on certain events and not others. For instance it may be desired to restrict certain key-presses in a text-input field. Examples include:

Credit card entry, numbers and spaces Phone numbers with international codes () and + Hex editor, restrict to A-Z 0-5 and many more… Currently Elm doesn’t allow this behaviour because stopPropogation and preventDefault are specified at the time the event listener is registered, and before the event listener has received any events.

I suggest adding a new Options type, something like this:

type alias EventOptions = { stopPropagation : Json.Decoder Bool , preventDefault : Json.Decoder Bool }

and a companion method:

onWithEventOptions

This will allow stopPropogation and preventDefault flags to be based on the content of the event received.

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 16
  • Comments: 27 (2 by maintainers)

Commits related to this issue

Most upvoted comments

The upcoming API in 0.19 has onBubble and onCapture that allow more flexibility.

In particular, they both take a Handler that gives you the ability to do stopPropagation and preventDefault conditionally. This design also means we can create “passive” listeners automatically, making scrolling smoother in touch devices.

If you think there are cases that the new API will not cover, please share them here. If others validate that the new API would not cover them, please open a new issue outlining the scenario before 0.19 comes out!

Here’s the work-around we’ve been using to preventDefault on only certain "keydown" events.

First, on the intended target element, do a “normal” on "keydown" with a message for which your update function will identify the desired keystrokes and respond to them, and ignore keystrokes that are not of interest to you. This listener allows all "keydown" events to continue propagating with their default action enabled.

input
    [ type "text"
    , on "keydown" (Decode.map KeyDown keyCode)
    ]
    []

Then, wrap the target element in a parent element with its own "keydown" event listener, this time using onWithOptions to prevent the default action for all successfully decoded events. The trick, then (with thanks to @opsb for this tip) is to only decode successfully the keycodes for which we wish to prevent the default action:

div
    [ preventDefaultUpDown
    ]
    [ input
        [ type "text"
        , on "keydown" (Decode.map KeyDown keyCode)
        ]
        []
    ]

⋮

preventDefaultUpDown : Html.Attribute (Msg record)
preventDefaultUpDown =
    let
        options =
            { Html.Events.defaultOptions | preventDefault = True }

        filterKey code =
            if code == KeyCodes.upArrow || code == KeyCodes.downArrow then
                Ok code
            else
                Err "ignored input"

        decoder =
            keyCode
                |> Decode.andThen (filterKey >> DecodeExtra.fromResult)
                |> Decode.map (always NoOp)
    in
        onWithOptions "keydown" options decoder

Note that successfully-decoded events result in NoOp messages, which do nothing in the update function.

All "keydown" events will therefore be processed by the event listener on the target element, and then those events will bubble up to the parent element, where only those "keydown" events for which you want to preventDefault will be processed with preventDefault = True.

It’s a little kludgey, but it works.

I can’t make any judgement on the proposed way/solution, but

allow stopPropogation and preventDefault flags to be based on the content of the event received.

is a super-essential, must-have, uber-needed feature.

This seems like a sweet API! Since this is an unusual and advanced feature, I’d personally just replace the existing API with this one.

It wouldn’t be much trouble to call onWithOptions passing { stopPropagation = Decode.succeed True } in the current case.

The work around I ended up using was to add an inline JavaScript event handler like so: Runnable example: https://ellie-app.com/3t42RzdkDQRa1/7

module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)


main : Program Never Model Msg
main =
    beginnerProgram { model = init, view = view, update = update }


type alias Model =
    { counter : Int
    }


init : Model
init =
    { counter = 0
    }


type Msg
    = NoOp
    | Increment


update msg model =
    case msg of
        NoOp ->
            model

        Increment ->
            { model | counter = model.counter + 1 }


view : Model -> Html Msg
view model =
    let
        inlineOnClick =
            -- the inline onclick *attribute* needs to come before the elm onclick handler
            -- you can inline the function directly or refer to a global function by name eg:
            -- [ attribute "onclick" "preventDefaultIfCtrl(event)" ]
            [ attribute "onclick" "if (event.ctrlKey) { event.preventDefault(); event.stopImmediatePropagation(); }" ]

        buttonAttrs =
            List.concat [ inlineOnClick, [ onClick Increment ] ]
    in
        div []
            [ p [] [ text "Hold down the `CTRL` key when clicking the increment button to prevent the default behavior" ]
            , button buttonAttrs [ text "Increment" ]
            , div [ class "count" ]
                [ text ("Count: " ++ toString model.counter)
                ]
            ]

Another case here: an autocomplete field needs to support ‘up’ and ‘down’ to navigate through result, while keeping the cursor position in the input field. Currently pressing ‘up’ would make the cursor goes to the leftmost position. You need a conditional stopPropgation() here to allow typing of character but ‘up’ and ‘down’

@sentience Brilliant!!! (https://stackoverflow.com/questions/42390708/elm-content-editable-with-enter-key/42446875#42446875)

My use case for this feature is with contenteditable divs. I want to trap an <enter> and instead of entering a char 13 into the text, use this as a trigger to blur the selected node and save the evt.target.text value back to the model

@inactivist that’s a sarcastic comment, it means we shouldn’t hope to see this feature anytime soon. “Compilation into WebAssembly” is used to crude how long it might take because, obviously, WebAssembly is not even on the roadmap either.

I’ve heard that it will be implemented natively in WebAssembly release

On Sun, Jan 15, 2017 at 10:06 PM, Matthias Urlichs <notifications@github.com

wrote:

OK, we’ve seen a lot of use cases here. I could contribute my own but that wouldn’t be helpful.

Any progress on actually solving this problem? Other than using “real” JS to handle the event in question … always a possible workaround, but not one I’m particularly fond of.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/elm-lang/virtual-dom/issues/18#issuecomment-272700869, or mute the thread https://github.com/notifications/unsubscribe-auth/ABd9zIAmJB649KC6pnqdvg_IJKebJYzFks5rSjXqgaJpZM4IiCGq .

One for the examples list:

I have a textarea and I want to trigger an update when the enter key is hit but prevent the newline from being added to the textarea.

Hi guys!

We get into situation where we have an Elm widget inside a form and which should process Enter key press event in its inputs, perform custom logic and prevent form submission. The thing is that we can’t select event on which we want to preventDefault and effectively blocking all input in those inputs.

I think another possible signature could be

onAGoodName : String -> Json.Decoder (Options, msg) -> Html.Attribute msg

Another scenario I came across recently: application hotkeys, you only want to preventDefault for certain keys.

Is this on the road-map for Elm 0.18? This would be the most important feature for me to have.

Basically, preventing the default for input elements is not possible, because as of now it will prevent Tab navigation. Preventing browser actions conditionally is all over the JS world and a must-have.