Interesting issue and excellent problem illustration!
The problem is that for some reason re-rendering doesn't occur when one of the selectionStart/selectionEnd properties remains the same. Try changing 5 to 6 on line #42.
It works when you introduce a forced reflow in the element structure. See here: https://ellie-app.com/6Q7h7Lm9XRya1 (I updated it to 0.19 to see if that would solve the problem, but it didn't).
Note that this probably re-renders the whole textarea anew so it might cause problems if the textarea is a huge piece of code. You could solve that by alternating between two identical textareas where you toggle their visibility every render.
module Main exposing (Model, Msg(..), main, update, view)
-- Note: this is Elm 0.19
import Browser
import Browser.Dom exposing (focus)
import Html exposing (Html, button, div, text, textarea)
import Html.Attributes exposing (attribute, class, cols, id, property, rows, style, value)
import Html.Events exposing (onClick)
import Html.Lazy exposing (lazy2)
import Json.Encode as Encode
import Task exposing (attempt)
type alias Model =
{ content : String
, selectionStart : Int
, selectionEnd : Int
-- keep counter of renderings for purposes of randomness in rendering loop
, renderCounter : Int
}
main =
Browser.element
{ init = initModel
, view = view
, update = update
, subscriptions = \s -> Sub.none
}
initModel : () -> ( Model, Cmd Msg )
initModel flags =
( Model "" 0 0 0, Cmd.batch [] )
type Msg
= Setup
| Unindent
| NoOp (Result Browser.Dom.Error ())
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
newRenderCounter =
model.renderCounter + 1
in
case msg of
Setup ->
( { model
| content = "\tabc\n\tdef\n\tghi"
, selectionStart = 5
, selectionEnd = 12
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
Unindent ->
( { model
| content = "\tabc\ndef\nghi"
, selectionStart = 5
, selectionEnd = 10
, renderCounter = newRenderCounter
}
, attempt NoOp <| focus "ta"
)
NoOp _ ->
( model, Cmd.batch [] )
view : Model -> Html Msg
view model =
div []
(viewTextarea model model.renderCounter
++ [ button [ onClick Setup ] [ text "Setup" ]
, button [ onClick Unindent ] [ text "Unindent" ]
]
)
viewTextarea : Model -> Int -> List (Html msg)
viewTextarea model counter =
let
rerenderForcer =
div [attribute "style" "display: none;"] []
ta =
textarea
[ id "ta"
, cols 40
, rows 20
, value model.content
, property "selectionStart" <| Encode.int model.selectionStart
, property "selectionEnd" <| Encode.int model.selectionEnd
]
[]
in
-- this is the clue. by alternating this every render, it seems to force Elm to render the textarea anew, fixing the issue. Probably not very performant though. For a performant version, use an identical textarea instead of the div and make sure the two selectionStart/end properties both differ from the previous iteration. Then alternate visibility between the two every iteration.
if isEven counter then
[ ta, rerenderForcer ]
else
[ rerenderForcer, ta ]
isEven : Int -> Bool
isEven i =
modBy 2 i == 0
tab
andshift
-tab
for me leaves focus (with text selected or not selected). Pressing setup then unindent tabs all lines and then unindent untabs the second two lines. – Lifeordeath