Search code examples
elm

Elm onInput character one behind


What am I doing wrong that is causing the value reported by onInput to be a character behind?

For example, type "mil" in the text field to filter to the mileposts row. Then delete it back to nothing and you'll see its still filtering mileposts (also see the browser console to see that value is still "m" even thought the text field is visibly "")

module Main exposing (main)

import Browser
import Html exposing (Html, a, button, div, input, li, span, text, ul)
import Html.Attributes exposing (checked, class, classList, placeholder, style, type_, value)
import Html.Events exposing (custom, onBlur, onClick, onFocus, onInput)
import Json.Decode as Json


type alias Layer =
    { name : String
    , description : String
    , selected : Bool
    }


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = \_ -> Sub.none
        }


type alias Model =
    { open : Bool
    , layers : List Layer
    , filtered : List Layer
    , searchText : String
    , highlightedIndex : Int
    }


init : () -> ( Model, Cmd Msg )
init _ =
    let
        layers =
            [ { name = "Parcels", description = "Show parcel lines", selected = False }
            , { name = "Mileposts", description = "Show Mile post markers", selected = False }
            ]
    in
    ( { open = False, layers = layers, filtered = layers, searchText = "", highlightedIndex = 0 }, Cmd.none )


type Msg
    = Open
    | Close
    | Change String
    | Up
    | Down
    | Toggle


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    let
        lastIndex =
            List.length model.filtered - 1
    in
    case msg of
        Open ->
            ( { model | open = True }, Cmd.none )

        Close ->
            ( { model | open = False }, Cmd.none )

        Change value ->
            let
                filtered =
                    model.layers
                        |> List.filter
                            (\{ name } ->
                                let
                                    _ =
                                        Debug.log "name" name

                                    _ =
                                        Debug.log "searchText" model.searchText
                                in
                                String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
                            )
            in
            ( { model | searchText = value, filtered = filtered }, Cmd.none )

        Up ->
            if model.highlightedIndex == 0 then
                ( { model | highlightedIndex = lastIndex }, Cmd.none )

            else
                ( { model | highlightedIndex = model.highlightedIndex - 1 }, Cmd.none )

        Down ->
            if model.highlightedIndex == lastIndex then
                ( { model | highlightedIndex = 0 }, Cmd.none )

            else
                ( { model | highlightedIndex = model.highlightedIndex + 1 }, Cmd.none )

        Toggle ->
            let
                highlightedLayer =
                    model.filtered
                        |> List.indexedMap Tuple.pair
                        |> List.filterMap
                            (\( idx, layer ) ->
                                if idx == model.highlightedIndex then
                                    Just layer

                                else
                                    Nothing
                            )

                updatedFiltered =
                    model.filtered
                        |> List.indexedMap
                            (\idx layer ->
                                if idx == model.highlightedIndex then
                                    { layer | selected = not layer.selected }

                                else
                                    layer
                            )

                updatedLayers =
                    model.layers
                        |> List.map
                            (\layer ->
                                if [ layer ] == highlightedLayer then
                                    { layer | selected = not layer.selected }

                                else
                                    layer
                            )
            in
            ( { model | filtered = updatedFiltered, layers = updatedLayers }, Cmd.none )


view model =
    div []
        [ span [ class "mapboxgl-ctrl-geocoder--icon mapboxgl-ctrl-geocoder--icon-search" ]
            [ span [ class "ds-badge ds-badge--red ds-badge--circle", style "margin-top" "-2px" ] [ text "3" ]
            ]
        , input
            [ type_ "text"
            , class "mapboxgl-ctrl-geocoder--input"
            , placeholder "Search layers"
            , value model.searchText
            , onFocus Open
            , style "padding-left" "45px"
            , onInput Change

            --, onKey [(38, Up), (40, Down), (13, Toggle)]
            ]
            []
        , if model.open then
            div [ class "suggestions-wrapper" ]
                [ ul [ class "suggestions", style "display" "block" ]
                    (model.filtered
                        |> List.indexedMap
                            (\idx { name, description, selected } ->
                                li [ classList [ ( "active", model.highlightedIndex == idx ) ] ]
                                    [ a []
                                        [ div [ class "mapboxgl-ctrl-geocoder--suggestion flex flex-column" ]
                                            [ div [] [ input [ type_ "checkbox", style "margin-top" "5px", checked selected ] [] ]
                                            , div [ class "ml-1" ]
                                                [ div [ class "mapboxgl-ctrl-geocoder--suggestion-title" ] [ text name ]
                                                , div [ class "mapboxgl-ctrl-geocoder--suggestion-address" ] [ text description ]
                                                ]
                                            ]
                                        ]
                                    ]
                            )
                    )
                ]

          else
            text ""
        ]


onKey : List ( Int, Msg ) -> Html.Attribute Msg
onKey codes =
    let
        isEnterKey keyCode =
            case codes |> List.filter (\( code, _ ) -> code == keyCode) of
                [ ( _, msg ) ] ->
                    Json.succeed
                        { message = msg
                        , stopPropagation = True
                        , preventDefault = True
                        }

                _ ->
                    Json.fail "silent failure :)"
    in
    custom "keydown" <|
        Json.andThen isEnterKey Html.Events.keyCode


options =
    { stopPropagation = True
    , preventDefault = True
    }

https://ellie-app.com/fcgHCF2z5sza1


Solution

  • The problem is here, when handling the Change message from onInput:

            Change value ->
                let
                    filtered =
                        model.layers
                            |> List.filter
                                (\{ name } ->
                                    let
                                        _ =
                                            Debug.log "name" name
    
                                        _ =
                                            Debug.log "searchText" model.searchText
                                    in
                                    String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
                                )
                in
                ( { model | searchText = value, filtered = filtered }, Cmd.none )
    

    You're using model.searchText to filter the list, binding the result to filtered, then updating model with the new searchText and filtered list. model.searchText still has the previous value when you're filtering. Use value instead when filtering, then it works as expected.