How to set focus on an element in Elm?
Asked Answered
D

6

47

How can I set the focus on a Html element in Elm? I tried to set the autofocus attribute on the element and it only sets the focus on the page load.

Drumfish answered 9/8, 2015 at 6:3 Comment(0)
V
56

The focus function in the elm-lang/dom package is used to set focus with a Task (without using any ports or JavaScript).

Internally it uses requestAnimationFrame to ensure any new DOM updates are rendered before it tries to find the DOM node to focus on.

An example use:

type Msg
    = FocusOn String
    | FocusResult (Result Dom.Error ())

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        FocusOn id ->
            ( model, Dom.focus id |> Task.attempt FocusResult )

        FocusResult result ->
            -- handle success or failure here
            case result of
                Err (Dom.NotFound id) ->
                    -- unable to find dom 'id'
                Ok () ->
                    -- successfully focus the dom

Full example on Ellie

Vesting answered 9/9, 2016 at 21:0 Comment(5)
Note, this does work, but I found it can silently fail at unexpected times if the element is added dynamically.Enid
Do you have an example where this fails?Vesting
Unfortunately, I don't have a reproducible example right now. I'll post back if I find another. Overall, this is definitely the right way of doing it, so my comment should be ignored until I can find a specific case :)Enid
"setting focus can silently fail if the element is invisible." package.elm-lang.org/packages/elm-lang/dom/latest/Dom#focusGirgenti
If using Elm 0.19, this package is now part of elm/browser.Tate
T
10

A workaround for this is to use Mutation Observers. Insert this JavaScript either in your main HTML page or in the main view of your Elm code:

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    handleAutofocus(mutation.addedNodes);
  });
});
var target = document.querySelector('body > div');
var config = { childList: true, subtree: true };
observer.observe(target, config);

function handleAutofocus(nodeList) {
  for (var i = 0; i < nodeList.length; i++) {
    var node = nodeList[i];
    if (node instanceof Element && node.hasAttribute('data-autofocus')) {
      node.focus();
      break;
    } else {
      handleAutofocus(node.childNodes);
    }
  }
}

Then create HTML elements by including Html.Attributes.attribute "data-autofocus" "".

Toxicogenic answered 29/10, 2015 at 0:13 Comment(0)
C
5

With elm/html 0.19 you can set the Html.Attrbutes autofocus to True

input [ onInput Code, autofocus True ] []
Chromous answered 3/10, 2018 at 9:44 Comment(3)
This doesn't appear to work reliably, I opened an issue here.Azotize
Yes the Dom.focus id |> Task.attempt FocusResult is more reliableChromous
This also needs import Browser.Dom in the file, example here package.elm-lang.org/packages/elm/core/latest/Task#attemptGeometer
I
4

I spent quite a bit of time exploring this recently. Unfortunately, I don't think it is possible with the existing elm-html library. However, I came up with a hack that utilizes css animations to trigger an event and embed that in pure js.

Here is my hack in Elm using a script node and a style node. It is very ugly in my opinion.

import Html exposing (div, button, text, input, node)
import Html.Events exposing (onClick)
import Html.Attributes exposing (type', class)
import StartApp.Simple

main =
  StartApp.Simple.start { model = model, view = view, update = update }

model = []

view address model =
  -- View now starts with a <style> and <script> (hacky)
  (node "style" [] [ Html.text style ]) ::
  (node "script" [] [Html.text script ]) ::
  (button [ onClick address AddInput ] [ text "Add Input" ]) ::
  model |>
  div []    

type Action = AddInput 

update action model =
  case action of
    AddInput -> (Html.p [] [input [type' "text", class "focus"] []]) :: model

-- Use pure string css (hacky)

style = """
.focus {
  animation-name: set-focus;
  animation-duration: 0.001s;
  -webkit-animation-name: set-focus;
  -webkit-animation-duration: 0.001s;
}
@-webkit-keyframes set-focus {
    0%   {color: #fff}
}
@keyframes set-focus {
    0%   {color: #fff}
}
"""

-- Cheating by embedding pure javascript... (hacky)

script = """
var insertListener = function(event){
 if (event.animationName == "set-focus") {
   event.target.focus();
 }               
}
document.addEventListener("animationstart", insertListener, false); // standard + firefox
document.addEventListener("MSAnimationStart", insertListener, false); // IE
document.addEventListener("webkitAnimationStart", insertListener, false); // Chrome + Safari
"""
Indeterminacy answered 9/8, 2015 at 14:30 Comment(0)
L
3

In Elm 0.19, use Browser.Dom.focus:

import Browser.Dom as Dom
import Task

type Msg
    = NoOp

focusSearchBox : Cmd Msg
focusSearchBox =
    Task.attempt (\_ -> NoOp) (Dom.focus "search-box")

You can choose to ignore if focusing fails like above or do something by triggering an update message.

Luzluzader answered 10/5, 2020 at 13:52 Comment(0)
S
1

Elm 0.19's Browser.Dom.focus is the modern solution

import Browser.Dom as Dom
import Task

type Msg
    = NoOp
    | Focus String

focusElement : String -> Cmd Msg
focusElement htmlId =
    Task.attempt (\_ -> NoOp) (Dom.focus htmlId)


update : Msg -> Model -> (Model, Cmd Msg)
update msg =
    case msg of
        Focus htmlId ->
            ( model, focusElement htmlId )
        NoOp ->
            ( model, Cmd.none )
Steffaniesteffen answered 15/5, 2022 at 2:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.