Add UpdateConfig

This commit is contained in:
Tom Nunn 2022-07-11 09:58:56 +02:00
parent 672ddbae22
commit d8cea06f84
12 changed files with 230 additions and 51 deletions

View File

@ -8,7 +8,8 @@
"Select",
"Select.Filter",
"Select.Effect",
"Select.Request"
"Select.Request",
"Select.UpdateConfig"
],
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {

View File

@ -63,7 +63,13 @@ update : Msg -> Model -> ( Model, MyEffect )
update msg model =
case msg of
SelectMsg subMsg ->
Select.Effect.updateWithRequest (Select.Effect.request FetchCocktails) subMsg model.select
Select.Effect.updateWith
{ request = Just (Select.Effect.request FetchCocktails)
, clearInputValueOnBlur = True
, selectExactMatchOnBlur = False
}
subMsg
model.select
|> Tuple.mapBoth (\select -> { model | select = select }) SelectEffect

View File

@ -11,6 +11,7 @@ import Http
import Json.Decode as Decode exposing (Decoder)
import Resources.ClearButton
import Select exposing (OptionState(..), Select)
import Select.UpdateConfig as SelectConfig
main : Program () Model Msg
@ -62,7 +63,13 @@ update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SelectMsg subMsg ->
Select.updateWithRequest SelectMsg (Select.request fetchCocktails) subMsg model.select
Select.updateWith
(SelectConfig.default
|> SelectConfig.withRequest (Select.request fetchCocktails)
)
SelectMsg
subMsg
model.select
|> Tuple.mapFirst (\select -> { model | select = select })

View File

@ -1,6 +1,7 @@
module Internal.Model exposing
( Model
, applyFilter
, blur
, clear
, closeMenu
, highlightIndex
@ -36,10 +37,11 @@ module Internal.Model exposing
import Browser.Dom as Dom
import Internal.Filter as Filter exposing (Filter)
import Internal.Option exposing (Option)
import Internal.Option as Option exposing (Option)
import Internal.OptionState exposing (OptionState(..))
import Internal.Placement exposing (Placement(..))
import Internal.RequestState exposing (RequestState(..))
import Select.UpdateConfig exposing (UpdateConfig)
@ -294,6 +296,36 @@ setFocused v (Model model) =
Model { model | focused = v }
blur : UpdateConfig effect -> List (Option a) -> Model a -> Model a
blur { clearInputValueOnBlur, selectExactMatchOnBlur, request } filteredOptions (Model model) =
(if model.selected == Nothing then
case ( selectExactMatchOnBlur, Option.findByString filteredOptions model.inputValue ) of
( True, Just option ) ->
selectOption option (Model model)
_ ->
if clearInputValueOnBlur then
Model
{ model
| inputValue = ""
, items =
if request == Nothing then
model.items
else
[]
}
else
Model model
else
Model model
)
|> setFocused False
|> closeMenu
-- INTERNAL

View File

@ -9,7 +9,7 @@ type Msg a
| OptionClicked (Option a)
| InputFocused
| InputClicked
| InputLostFocus
| InputLostFocus (List (Option a))
| MouseEnteredOption Int
| KeyDown (List (Option a)) String
| GotContainerAndMenuElements (Result Dom.Error { menu : Dom.Viewport, container : Dom.Element })

View File

@ -1,5 +1,33 @@
module Internal.Option exposing (Option)
module Internal.Option exposing (Option, findByString, findByValue)
type alias Option a =
( a, String )
findByValue : List (Option a) -> a -> Maybe (Option a)
findByValue list a =
case list of
[] ->
Nothing
x :: xs ->
if a == Tuple.first x then
Just x
else
findByValue xs a
findByString : List (Option a) -> String -> Maybe (Option a)
findByString list s =
case list of
[] ->
Nothing
x :: xs ->
if String.toLower s == String.toLower (Tuple.second x) then
Just x
else
findByString xs s

View File

@ -6,10 +6,11 @@ import Internal.Msg exposing (Msg(..))
import Internal.Option exposing (Option)
import Internal.Request as Request exposing (Request)
import Internal.RequestState exposing (RequestState(..))
import Select.UpdateConfig exposing (UpdateConfig)
update : Maybe (Request effect) -> Msg a -> Model a -> ( Model a, Effect effect )
update maybeRequest msg model =
update : UpdateConfig effect -> Msg a -> Model a -> ( Model a, Effect effect )
update ({ request } as config) msg model =
case msg of
InputChanged val ->
( model
@ -18,20 +19,20 @@ update maybeRequest msg model =
|> Model.applyFilter True
|> Model.setSelected Nothing
|> Model.setItems
(if maybeRequest /= Nothing && val == "" then
(if request /= Nothing && val == "" then
[]
else
Model.toItems model
)
|> Model.setRequestState
(if maybeRequest /= Nothing then
(if request /= Nothing then
Just NotRequested
else
Nothing
)
, case maybeRequest of
, case request of
Just req ->
if String.length val >= Request.toMinLength req then
Effect.Debounce (Request.toDelay req) val
@ -49,14 +50,13 @@ update maybeRequest msg model =
)
InputFocused ->
onFocusMenu maybeRequest model
onFocusMenu request model
InputClicked ->
onFocusMenu maybeRequest model
onFocusMenu request model
InputLostFocus ->
( Model.setFocused False model
|> Model.closeMenu
InputLostFocus filteredOptions ->
( Model.blur config filteredOptions model
, Effect.none
)
@ -82,7 +82,7 @@ update maybeRequest msg model =
( model
|> Model.clear
|> Model.setItems
(if maybeRequest == Nothing then
(if request == Nothing then
Model.toItems model
else
@ -94,7 +94,7 @@ update maybeRequest msg model =
InputDebounceReturned val ->
if val == Model.toInputValue model then
( Model.setRequestState (Just Loading) model
, Maybe.map (Request.toEffect >> (\effect -> Effect.Request (effect val))) maybeRequest
, Maybe.map (Request.toEffect >> (\effect -> Effect.Request (effect val))) request
|> Maybe.withDefault Effect.none
)
@ -108,7 +108,11 @@ update maybeRequest msg model =
( model
|> Model.setItems items
|> Model.setRequestState (Just Success)
, getContainerAndMenuElementsEffect model
, if Model.toValue model == Nothing then
getContainerAndMenuElementsEffect model
else
Effect.none
)
Err _ ->

View File

@ -16,7 +16,7 @@ import Html.Events
import Internal.Filter as Filter exposing (Filter)
import Internal.Model as Model exposing (Model)
import Internal.Msg exposing (Msg(..))
import Internal.Option exposing (Option)
import Internal.Option as Option exposing (Option)
import Internal.OptionState exposing (OptionState(..))
import Internal.Placement as Placement exposing (Placement)
import Internal.RequestState exposing (RequestState(..))
@ -132,7 +132,7 @@ inputView filteredOptions model config =
(config.inputAttribs
++ [ Events.onFocus (config.onChange InputFocused)
, Events.onClick (config.onChange InputClicked)
, Events.onLoseFocus (config.onChange InputLostFocus)
, Events.onLoseFocus (config.onChange (InputLostFocus filteredOptions))
, onKeyDown (KeyDown filteredOptions >> config.onChange)
, Element.htmlAttribute (Html.Attributes.id <| Model.toInputElementId model)
, Element.inFront <|
@ -359,16 +359,8 @@ hijackKey tagger key =
findOptionString : List (Option a) -> a -> Maybe String
findOptionString list a =
case list of
[] ->
Nothing
x :: xs ->
if a == Tuple.first x then
Just (Tuple.second x)
else
findOptionString xs a
Option.findByValue list a
|> Maybe.map Tuple.second
ariaLive : Int -> Element msg

View File

@ -3,8 +3,8 @@ module Select exposing
, setItems, setSelected, setInputValue, closeMenu
, toValue, toInputValue
, isMenuOpen, isLoading, isRequestFailed
, Msg, update, updateWithRequest, Request, request, gotRequestResponse
, ViewConfig, view, withMenuAttributes, MenuPlacement(..), withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, OptionState, withOptionElement, ClearButton, withClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPositionFixed
, Msg, update, updateWith, Request, request, gotRequestResponse
, ViewConfig, view, withMenuAttributes, MenuPlacement(..), withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, withOptionElement, OptionState, withClearButton, ClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPositionFixed
, toElement
, Effect
)
@ -34,12 +34,12 @@ module Select exposing
# Update and Requests
@docs Msg, update, updateWithRequest, Request, request, gotRequestResponse
@docs Msg, update, updateWith, Request, request, gotRequestResponse
# Configure View
@docs ViewConfig, view, withMenuAttributes, MenuPlacement, withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, OptionState, withOptionElement, ClearButton, withClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPositionFixed
@docs ViewConfig, view, withMenuAttributes, MenuPlacement, withMenuMaxHeight, withMenuMaxWidth, withNoMatchElement, withOptionElement, OptionState, withClearButton, ClearButton, clearButton, withFilter, withMenuAlwaysAbove, withMenuAlwaysBelow, withMenuPositionFixed
# Element
@ -64,6 +64,7 @@ import Internal.Request as Request
import Internal.Update as Update
import Internal.View as View exposing (ViewConfigInternal)
import Select.Filter exposing (Filter)
import Select.UpdateConfig as UpdateConfig exposing (UpdateConfig)
@ -176,18 +177,25 @@ type alias Msg a =
-}
update : (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Cmd msg )
update tagger msg select =
Update.update Nothing msg select
Update.update UpdateConfig.default msg select
|> Tuple.mapSecond (Effect.perform tagger (\_ -> Cmd.none))
{-| Update with an HTTP request to retrieve matching remote results.
{-| Update with configuration options, including using an HTTP request to retrieve matching remote results.
Note that in order to avoid an elm/http dependency in this package, you will need to provide the request Cmd yourself.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Request SelectMsg subMsg ->
Select.updateWithRequest SelectMsg (Select.request fetchThings) subMsg model.select
Select.updateWith
{ request = Select.request fetchThings
, clearInputValueOnBlur = False
, selectExactMatchOnBlur = True
}
SelectMsg
subMsg
model.select
|> Tuple.mapFirst (\select -> { model | select = select })
fetchThings : String -> Cmd (Select.Msg Thing)
@ -197,10 +205,21 @@ Note that in order to avoid an elm/http dependency in this package, you will nee
, expect = Http.expectJson (Select.gotRequestResponse query) (Decode.list thingDecoder)
}
You can also use [Select.UpdateConfig](Select-UpdateConfig) to build up a config:
Select.updateWith
(Select.UpdateConfig.default
|> Select.UpdateConfig.withRequest (Select.request fetchThings)
)
SelectMsg
subMsg
model.select
|> Tuple.mapFirst (\select -> { model | select = select })
-}
updateWithRequest : (Msg a -> msg) -> Request a -> Msg a -> Select a -> ( Select a, Cmd msg )
updateWithRequest tagger req msg select =
Update.update (Just req) msg select
updateWith : UpdateConfig (Cmd (Msg a)) -> (Msg a -> msg) -> Msg a -> Select a -> ( Select a, Cmd msg )
updateWith config tagger msg select =
Update.update config msg select
|> Tuple.mapSecond (Effect.perform identity identity >> Cmd.map tagger)

View File

@ -1,6 +1,6 @@
module Select.Effect exposing
( Effect
, update, updateWithRequest, Request, request
, update, updateWith, Request, request
, perform, performWithRequest
)
@ -14,7 +14,7 @@ module Select.Effect exposing
# Update Effect
@docs update, updateWithRequest, Request, request
@docs update, updateWith, Request, request
# Perform Effect
@ -28,6 +28,7 @@ import Internal.Model exposing (Model)
import Internal.Msg exposing (Msg)
import Internal.Request as Request
import Internal.Update as Update
import Select.UpdateConfig as UpdateConfig exposing (UpdateConfig)
{-| The Effect type
@ -62,7 +63,7 @@ type alias Effect effect =
-}
update : Msg a -> Select a -> ( Select a, Effect Never )
update =
Update.update Nothing
Update.update UpdateConfig.default
{-| Update with an HTTP request. Note that in order to avoid an elm/http dependency in this package, you will need to provide the request Effect yourself.
@ -96,9 +97,9 @@ update =
}
-}
updateWithRequest : Request effect -> Msg a -> Select a -> ( Select a, Effect effect )
updateWithRequest req =
Update.update (Just req)
updateWith : UpdateConfig effect -> Msg a -> Select a -> ( Select a, Effect effect )
updateWith =
Update.update
{-| A request that uses your Effect type

View File

@ -6,11 +6,15 @@ module Select.Request exposing (withDelay, withMinLength)
update msg model =
case msg of
SelectMsg subMsg ->
Select.updateWithRequest SelectMsg
(Select.request fetchThings
|> Select.Request.withDelay 200
|> Select.Request.withMinLength 4
Select.updateWith
(Select.UpdateConfig.default
|> Select.UpdateConfig.withRequest
(Select.request fetchThings
|> Select.Request.withDelay 200
|> Select.Request.withMinLength 4
)
)
SelectMsg
subMsg
model.select
|> Tuple.mapFirst (\select -> { model | select = select })

View File

@ -0,0 +1,85 @@
module Select.UpdateConfig exposing
( UpdateConfig, default
, withRequest, withClearInputValueOnBlur, withSelectExactMatchOnBlur
)
{-| Build a config for updating the select
# Type
@docs UpdateConfig, default
# Configure
@docs withRequest, withClearInputValueOnBlur, withSelectExactMatchOnBlur
-}
import Internal.Request exposing (Request)
{-| The UpdateConfig type
-}
type alias UpdateConfig effect =
{ request : Maybe (Request effect)
, clearInputValueOnBlur : Bool
, selectExactMatchOnBlur : Bool
}
{-| The default config. No request, clears the input on blur if no selection and does not select an exact string match on blur.
-}
default : UpdateConfig Never
default =
{ request = Nothing
, clearInputValueOnBlur = False
, selectExactMatchOnBlur = False
}
{-| Update with an HTTP request to retrieve matching remote results.
Note that in order to avoid an elm/http dependency in this package, you will need to provide the request Cmd yourself.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Request SelectMsg subMsg ->
Select.updateWith
(Select.UpdateConfig.default
|> Select.UpdateConfig.withRequest (Select.request fetchThings)
)
SelectMsg
subMsg
model.select
|> Tuple.mapFirst (\select -> { model | select = select })
fetchThings : String -> Cmd (Select.Msg Thing)
fetchThings query =
Http.get
{ url = "https://awesome-thing.api/things?search=" ++ query
, expect = Http.expectJson (Select.gotRequestResponse query) (Decode.list thingDecoder)
}
-}
withRequest : Request effect -> UpdateConfig eff1 -> UpdateConfig effect
withRequest request config =
{ request = Just request
, clearInputValueOnBlur = config.clearInputValueOnBlur
, selectExactMatchOnBlur = config.selectExactMatchOnBlur
}
{-| Should the input value be cleared when the input loses focus? Setting this to False can be useful to make the select more like an autocomplete
-}
withClearInputValueOnBlur : Bool -> UpdateConfig effect -> UpdateConfig effect
withClearInputValueOnBlur v config =
{ config | clearInputValueOnBlur = v }
{-| If the input value exactly matches the valueToString value of one of the options, set is as selected when the input loses focus.
-}
withSelectExactMatchOnBlur : Bool -> UpdateConfig effect -> UpdateConfig effect
withSelectExactMatchOnBlur v config =
{ config | selectExactMatchOnBlur = v }