How to implement onClick listener for only when clicked directly?

I need to detect a direct click on a list item. The goal is to edit the list items by clicking on them, and exit the edit mode by clicking again. Currently, when I edit an item and click the form, it closes the form. Of course, I only want to close it when I click on some empty space, not when I try to enter something into the form.

So I tried to implement an event listener that only fires when the item is directly clicked. My attempts to implement something similar to Html.Events.onClick have failed so far. This is my current code:

{-| Only fires for clicks exactly on the element.

See <https://javascript.info/bubbling-and-capturing#event-target> for further information.

-}
onDirectClick : Msg -> Attribute Msg
onDirectClick msg =
    let
        decoder =
            Decode.map2 (\current target -> current == target)
                (Decode.field "currentTarget" Decode.string)
                (Decode.field "target" Decode.string)
                |> Decode.andThen
                    (\isDirect ->
                        if isDirect then
                            Decode.succeed msg

                        else
                            Decode.fail "Is not a direct click."
                    )
    in
    on "click" decoder

When I click on the list item, nothing happens. I think it’s because currentTarget and target are not strings, but objects. But I couldn’t find any identifier field in the documentation that I could use to check whether they are the same.

How can I implement onDirectClick?

They are JS objects, so you can use Json.Dedode.value to just get them as Value and then compare them.

Thanks! I don’t know why I didn’t figure that out. :smile:

It works for elements that are the same, but when I click on the form (where the click is not direct), I get errors in the console.

Uncaught Error: Trying to use `(==)` on functions.
There is no way to know if functions are "the same" in the Elm sense.
Read more about this at https://package.elm-lang.org/packages/elm/core/latest/Basics#== which describes why it is this way and what the better version will look like.
Uncaught TypeError: Cannot read property 'click' of undefined
TypeError: y is undefined
TypeError: undefined has no properties

All the errors are rooted in the _Utils_eqHelp function. I believe that this means that Elm cannot compare two differently structured objects as easily as I assumed.

I will investigate this later.

I think the simplest thing is to stop propagation. Like this, no indirect click can fire if a direct click fired first. You can simply send the message defined for when the click happens.
Well It can be indirect if your list item has a dom sub hierarchy but I don’t think that’s what your intention really is right?

I’ve created an Elli that demonstrates the minimal setup for my struggle.

When you try to enter something in edit mode, the click causes the edit mode to close. And I don’t want to attach stopPropagationOn listeners, because I have multiple elements (accept & reset buttons) that would need that attached, too. So I think I really am looking for a listener on the container div that only fires when directly clicked.

I’m not to convinced about UX of having text switch from editable to not editable on click, instead of by another visual indicator, but for the sake of the exercise:

Here is a working example with what I meant by using stop propagation: https://ellie-app.com/3fhTC72Shwba1. With a helper function it’s really not such a hassle. (The div doesn’t occupy the whole page so click on the right of the text to trigger the div onclick)

I’ll see if I can have another version with current target checks.

form [] [ input [ stopOn "click" noMsg, ...

I would prefer to not “blacklist” every element that is in front of the div. If we manage to get a onDirectClick listener, that is the cleaner approach from my point of view.

An aside on my UX reasoning: I have a list of todos. The app is supposed to be heavily used on mobile. There, tapping a list item to expand and edit it (edit text, delete) makes sense. That is what I’m trying to implement.

And here is an example that uses the id of the element to check if it is the direct target: https://ellie-app.com/3fjt7546Pkga1 (had to add things below because the form element is taking the full width, maybe it can be shrinked but I’m not a css ninja so no idea ^^).

Your idea is a good workaround! I modified it slightly, so that only the div needs an id and fixed the issue with the sizing: https://ellie-app.com/3fkYKTjpPkMa1

While this idea works quite well around the problem, I would still prefer the cleaner approach that consists of simply the onDirectClick listener. :wink: But thanks for that workaround!

Yes the multiples ids were not needed ahah that was just for debugging the output in console ^^. (the NoMsg isn’t needed as well btw, leftover of previous example).

---- Edit
You can slightly improve this by comparing the id of target and currentTarget instead of passing one id as the argument of the decoder. But this still needs you to set an id, otherwise you have "" == "" which is always true.

---- End edit

I think you won’t find a pure elm solution for your problem for the simple reason that elm cannot compare Value elements (cf equality doc). So the only way of identifying that two dom elements are the same is through a unique identifier accessible at the decoding phase. I’ve quickly search to see if there was one such “automatically” available in browser nodes and didn’t find so I settled to set the id property myself.

PS, as you may have learned in the guide, using keyed views for things like lists is quite important to avoid some issues that may appear due to how the virtual dom works. Knowing this, it means that you will already have identifiers available so it plays nicely with this technique using the id to detect that current target is target.

Thanks for the link to the equality doc. That sounds like the best place to implement this listener would be in elm/html. I will see if I get around to making a feature proposal.

Thanks for your support! I learned a lot of things and have a decent workaround! :slight_smile:

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.