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
}
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.