Tag request responses to avoid race conditions

This commit is contained in:
Tom Nunn 2022-06-30 13:48:35 +02:00
parent 43ee7ff1ba
commit cda9089d43
9 changed files with 70 additions and 50 deletions

View File

@ -100,7 +100,7 @@ performEffect effect =
Select.Effect.performWithRequest performEffect selectEffect
FetchCocktails query ->
fetchCocktails (Select.gotRequestResponse >> SelectMsg) query
fetchCocktails (Select.gotRequestResponse query >> SelectMsg) query

View File

@ -71,7 +71,7 @@ fetchCocktails query =
Http.get
{ url = "https://thecocktaildb.com/api/json/v1/1/search.php?s=" ++ String.replace " " "+" query
, expect =
Http.expectJson Select.gotRequestResponse
Http.expectJson (Select.gotRequestResponse query)
(Decode.field "drinks"
(Decode.oneOf
[ Decode.list cocktailDecoder

View File

@ -5,12 +5,14 @@ module Internal.Model exposing
, closeMenu
, highlightIndex
, init
, isFocused
, isLoading
, isOpen
, isRequestFailed
, openMenu
, selectOption
, setElements
, setFocused
, setInputValue
, setItems
, setRequestState
@ -59,6 +61,7 @@ type alias InternalState a =
, menuElement : Maybe Dom.Element
, requestState : Maybe RequestState
, applyFilter : Bool
, focused : Bool
}
@ -79,6 +82,7 @@ init id =
, menuElement = Nothing
, requestState = Nothing
, applyFilter = False
, focused = False
}
@ -144,7 +148,10 @@ toOptionElementId (Model { id }) idx =
toOptionState : Model a -> ( Int, a ) -> OptionState
toOptionState (Model { highlighted, selected }) ( idx, a ) =
if highlighted == idx then
if highlighted == idx && selected == Just a then
SelectedAndHighlighted
else if highlighted == idx then
Highlighted
else if selected == Just a then
@ -163,6 +170,11 @@ isOpen (Model { menuOpen }) =
menuOpen
isFocused : Model a -> Bool
isFocused (Model { focused }) =
focused
isLoading : Model a -> Bool
isLoading (Model { requestState }) =
requestState == Just Loading
@ -277,6 +289,11 @@ applyFilter v (Model model) =
Model { model | applyFilter = v }
setFocused : Bool -> Model a -> Model a
setFocused v (Model model) =
Model { model | focused = v }
-- INTERNAL

View File

@ -15,5 +15,5 @@ type Msg a
| GotContainerAndMenuElements (Result Dom.Error { menu : Element, container : Element })
| ClearButtonPressed
| InputDebounceReturned String
| GotRequestResponse (Result () (List a))
| GotRequestResponse String (Result () (List a))
| NoOp

View File

@ -5,3 +5,4 @@ type OptionState
= Idle
| Highlighted
| Selected
| SelectedAndHighlighted

View File

@ -54,7 +54,8 @@ update tagger maybeRequest msg model =
onFocusMenu tagger maybeRequest model
InputLostFocus ->
( Model.closeMenu model
( Model.setFocused False model
|> Model.closeMenu
, Effect.none
)
@ -99,17 +100,23 @@ update tagger maybeRequest msg model =
else
( model, Effect.none )
GotRequestResponse (Ok items) ->
( model
|> Model.setItems items
|> Model.setRequestState (Just Success)
, getContainerAndMenuElementsEffect tagger model
)
GotRequestResponse inputVal response ->
if inputVal == Model.toInputValue model then
case response of
Ok items ->
( model
|> Model.setItems items
|> Model.setRequestState (Just Success)
, getContainerAndMenuElementsEffect tagger model
)
GotRequestResponse (Err _) ->
( Model.setRequestState (Just Failed) model
, Effect.none
)
Err _ ->
( Model.setRequestState (Just Failed) model
, Effect.none
)
else
( model, Effect.none )
NoOp ->
( model, Effect.none )
@ -117,7 +124,8 @@ update tagger maybeRequest msg model =
onFocusMenu : (Msg a -> msg) -> Maybe (Request effect) -> Model a -> ( Model a, Effect effect msg )
onFocusMenu tagger maybeRequest model =
( Model.highlightIndex 0 model
( Model.setFocused True model
|> Model.highlightIndex 0
, if maybeRequest == Nothing || Model.toRequestState model == Just Success then
getContainerAndMenuElementsEffect tagger model

View File

@ -10,7 +10,6 @@ import Element exposing (Attribute, Element)
import Element.Background as Background
import Element.Border as Border
import Element.Events as Events
import Element.Font as Font
import Element.Input as Input
import Html.Attributes
import Html.Events
@ -140,7 +139,7 @@ inputView placement filteredOptions model config =
)
{ onChange = InputChanged >> config.onChange
, text =
if Model.isOpen model then
if Model.isFocused model then
Model.toInputValue model
else
@ -307,35 +306,25 @@ positionFixedAttributes placement container =
defaultOptionElement : (a -> String) -> OptionState -> a -> Element msg
defaultOptionElement toString optionState a =
case optionState of
Highlighted ->
Element.el
([ Background.color (Element.rgb 0.9 0.9 0.9)
, Font.color (Element.rgb 0 0 0)
]
++ defaultOptionAttrs
)
(Element.text (toString a))
Element.el
[ Element.width Element.fill
, Element.pointer
, Element.paddingXY 14 10
, Background.color <|
case optionState of
Highlighted ->
Element.rgb 0.95 0.95 0.95
Selected ->
Element.el
([ Background.color (Element.rgb 0.65 0.84 0.98)
, Font.color (Element.rgb 0 0 0)
]
++ defaultOptionAttrs
)
(Element.text (toString a))
Selected ->
Element.rgba 0.64 0.83 0.97 0.8
Idle ->
Element.el defaultOptionAttrs (Element.text (toString a))
SelectedAndHighlighted ->
Element.rgba 0.64 0.83 0.97 1
defaultOptionAttrs : List (Attribute msg)
defaultOptionAttrs =
[ Element.width Element.fill
, Element.pointer
, Element.paddingXY 14 10
]
Idle ->
Element.rgb 1 1 1
]
(Element.text (toString a))
defaultNoMatchElement : Element msg

View File

@ -194,7 +194,7 @@ Note that in order to avoid an elm/http dependency in this package, you will nee
fetchThings query =
Http.get
{ url = "https://awesome-thing.api/things?search=" ++ query
, expect = Http.expectJson Select.gotRequestResponse (Decode.list thingDecoder)
, expect = Http.expectJson (Select.gotRequestResponse query) (Decode.list thingDecoder)
}
-}
@ -219,11 +219,12 @@ request =
Request.request
{-| Hook the request Cmd result back into update with this Msg
{-| Hook the request Cmd result back into update with this Msg. You need to pass in the string query (input value)
that was used for the request. This is used to match up the correct response to most recent request in case of race conditions.
-}
gotRequestResponse : Result err (List a) -> Msg a
gotRequestResponse =
Result.mapError (\_ -> ()) >> Msg.GotRequestResponse
gotRequestResponse : String -> Result err (List a) -> Msg a
gotRequestResponse inputValue =
Result.mapError (\_ -> ()) >> Msg.GotRequestResponse inputValue
@ -307,6 +308,7 @@ type OptionState
= Idle
| Highlighted
| Selected
| SelectedAndHighlighted
{-| Provide your own element to show when there are no matches based on the filter and input value. This appears below the input.
@ -379,3 +381,6 @@ mapOptionState state =
OptionState.Selected ->
Selected
OptionState.SelectedAndHighlighted ->
SelectedAndHighlighted

View File

@ -86,7 +86,7 @@ update tagger =
Select.Effect.performWithRequest performEffect selectEffect
FetchThings query ->
fetchThings (Select.gotRequestResponse >> SelectMsg) query
fetchThings (Select.gotRequestResponse query >> SelectMsg) query
fetchThings : (Result Http.Error (List thing) -> msg) -> String -> Cmd msg
fetchThings tagger query =