Gizra / elm-debouncer / Debouncer.Basic

Ths module allows you to managing inputs that occur over time, so that they get handed back to you, at future moments, grouped in a way that you have configured. Depending on the configuration you provide, you can use this to implemented debouncing, throttling, or other ways of managing inputs that occur over time.

This module provides the most general, comprehensive interface. You can choose:

For a simpler module that is focused on debouncing or throttling a Msg type, see the Debouncer.Messages module.

A quick note on terminology: the debouncer is said to be "unsettled" while it is collecting inputs and considering when to emit output. It becomes "settled" again once a specified period has passed without any inputs (see settleWhenQuietFor).

To use this module, you will need to integrate it into your Model and Msg type, and handle it in your update function. Here's one example, where the "input" you're providing is your own Msg type, and the output is also your Msg type. (To avoid some of the verbosity below, you can use the Debouncer.Messages module).

import Browser
import Debouncer.Basic as Debouncer exposing (Debouncer, fromSeconds, provideInput, settleWhenQuietFor, toDebouncer)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)

type alias Model =
    { quietForOneSecond : Debouncer Msg Msg
    , messages : List String
    }

init : ( Model, Cmd Msg )
init =
    ( { quietForOneSecond =
            Debouncer.manual
                |> settleWhenQuietFor (Just <| fromSeconds 1)
                |> toDebouncer
      , messages = []
      }
    , Cmd.none
    )

type Msg
    = MsgQuietForOneSecond (Debouncer.Msg Msg)
    | DoSomething

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        MsgQuietForOneSecond subMsg ->
            let
                ( subModel, subCmd, emittedMsg ) =
                    Debouncer.update subMsg model.quietForOneSecond

                mappedCmd =
                    Cmd.map MsgQuietForOneSecond subCmd

                updatedModel =
                    { model | quietForOneSecond = subModel }
            in
            case emittedMsg of
                Just emitted ->
                    update emitted updatedModel
                        |> Tuple.mapSecond (\cmd -> Cmd.batch [ cmd, mappedCmd ])

                Nothing ->
                    ( updatedModel, mappedCmd )

        DoSomething ->
            ( { model | messages = model.messages ++ [ "I did something" ] }
            , Cmd.none
            )

view : Model -> Html Msg
view model =
    div [ style "margin" "1em" ]
        [ button
            [ DoSomething
                |> provideInput
                |> MsgQuietForOneSecond
                |> onClick
            ]
            [ text "Click here repeatedly." ]
        , p [] [ text " I'll add a message below once you stop clicking for one second." ]
        , model.messages
            |> List.map (\message -> p [] [ text message ])
            |> div []
        ]

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

But, remember that you get to choose the types of the inputs and outputs. So, you can do something quite different from this example if you like.

Creating a debouncer


type alias Debouncer i o =
Debouncer.Internal.Debouncer i o

An opaque type which holds the state of a debouncer. You will need to integrate this into your Model type to keep the state.

The i type parameter represents the type of the inputs that you will provide. One common case will be that your inputs will be your Msg type -- that is, you'll be providing messages to "smooth out" over time. However, you're not limited to that -- you can provide any kind of input you like.

The o type parameter represents the type of the output which will be emitted by the debouncer. In many cases, this will be the same thing as your input type -- for instance, you might provide your Msg type and get your Msg type back. However, you're not limited to that -- you can get a different type back if you like. (You just have to provide an Accmulator function to accumulate your inputs into an output).

To create a Debouncer:

For instance:

manual
    |> settleWhenQuietFor (Just (fromSeconds 0.5))
    |> toDebouncer

... or, the equivalent:

debounce (fromSeconds 0.5)
    |> toDebouncer


type alias Config i o =
Debouncer.Internal.Config i o

An opaque type representing the configuration needed for a debouncer.

To create a debouncer:

For instance:

manual
    |> settleWhenQuietFor (Just (fromSeconds 2.0))
    |> emitWhileUnsettled (Just (fromSeconds 0.5))
    |> toDebouncer

toDebouncer : Config i o -> Debouncer i o

Initialize a Debouncer using the supplied Config.

For now, a debouncer's configuration cannot be changed once the debouncer is created. (This is a feature that could be provided in future, if desirable).

Creating a configuration

manual : Config i i

A starting point for configuring a debouncer that only emits when you tell it to, via emitNow.

By default, it:

So, without more, you would need to tell this debouncer when to emit something (via emitNow) -- it would never happen automatically.

To change any of those parameters, use the various functions that alter a Config (i.e. settleWhenQuietFor, emitWhenUnsettled, emitWhileUnsettled).

By default, the output type is the same as the input type. However, you can change that by using the accumulateWith function to provide a different accumulator.

debounce : Milliseconds -> Config i i

A starting point for a configuring a debouncer which debounces-- that is, which will emit once quiet for the time you specify.

So, debounce (fromSeconds 2) is equivalent to

manual
    |> settleWhenQuietFor (Just (fromSeconds 2))

If you also want to emit using the first input, then you can use emitWhenSettled. For instance, the following configuration would emit the first input immediately when becoming unsettled, and then emit any subsequent input once the debouncer was quiet for 2 seconds.

debounce (fromSeconds 2)
    |> emitWhenUnsettled (Just 0)

throttle : Milliseconds -> Config i i

A starting point for configuring a debouncer which throttles -- that is, which will emit the first input immediately, and then accmulate and emit no more often than the specified interval.

So, throttle (fromSeconds 2) is equivalent to

manual
    |> emitWhileUnsettled (Just (fromSeconds 2))
    |> emitWhenUnsettled (Just 0)


type alias Milliseconds =
Basics.Int

This is a handy type alias to note that we work in millisecond intervals.

fromSeconds : Basics.Float -> Milliseconds

A convenience when you'd rather think in seconds than milliseconds.

fromSeconds 0.5 --> 500

settleWhenQuietFor : Maybe Milliseconds -> Config i o -> Config i o

How long should the debouncer wait without input before becoming "settled" again?

If you are "debouncing" (i.e. emitWhileUnsettled is Nothing), then this is the key parameter controlling when you will receive output -- you'll receive the output after no inputs have been provided for the specified time.

If you are "throttling" (i.e. emitWhileUnsettled is notNothing), then this parameter won't make much difference, unless you are also specifying emitWhenUnsettled in order to do something with the initial input.

emitWhenUnsettled : Maybe Milliseconds -> Config i o -> Config i o

Should the debouncer do something special when it becomes unsettled?

If Nothing (the default), the debouncer emits only on the "trailing edge." That is, it won't do anything special with the first input -- it will just accumulate it and eventually emit according to the other configuration parameters.

If Just 0, the debouncer will immediately emit the first input when becoming unsettled. It will then remain unsettled, accumulating further input and eventually emitting it.

If Just interval, the debouncer will use the provided interval to emit after becoming unsettled. It will then remain unsettled, collecting further input and eventually emitting it.

emitFirstInput : Basics.Bool -> Config i o -> Config i o

Modify a Config by controlling whether to emit the first input when becoming unsettled.

emitFirstInput True config is equivalent to

emitWhenUnsettled (Just 0) config

emitFirstInput False config is equivalent to

emitWhenUnsettled Nothing config

For more complex cases, where you want to emit the first input on a different interval than others, but not immediately, you can use emitWhenSettled directly.

emitWhileUnsettled : Maybe Milliseconds -> Config i o -> Config i o

Should the debouncer emit while it is unsettled?

If Nothing (the default), the debouncer will wait until it becomes settled again before emitting. This is what you might refer to as "debouncing".

If Just interval, the debouncer will emit at the provided interval while it is unsettled. This is what you might refer to as "throttling".


type alias Accumulator i o =
Debouncer.Internal.Accumulator i o

A function with the signature i -> Maybe o -> Maybe o.

This function will be used to accumulate each provided input with the output so far. (If this is the first input since we've emitted something, the output so far will be Nothing).

For some standard accumulators, see lastInput, firstInput, allInputs, addInputs, appendInputToOutput and appendOutputToInput. But you're welcome to write your own, if none of those suit you.

accumulateWith : Accumulator i o -> Config a b -> Config i o

How should the debouncer combine new input with the output collected so far?

You can use several pre-built accumulators:

- `lastInput` (the default)
- `firstInput`
- `allInputs`
- `addInputs`
- `appendInputToOutput`
- `appendOutputToInput`

Or, if none of those suit, you can provide your own function of the form

i -> Maybe o -> Maybe o

The o will be Nothing if this is the first input since the debouncer has become unsettled.

Pre-built accumulators

lastInput : Accumulator i i

An accumulator which just keeps the last provided input. This is probably the one you'll want most often (and is the default), but you do have other choices.

firstInput : Accumulator i i

An accumulator which just keeps the first provided input. Thus, the remaining inputs are only used for timing purposes -- their values aren't actually remembered.

allInputs : Accumulator i (List i)

An accmulator which keeps all the inputs in a List. You can use this if you want the whole list once the deouncer emits its output. Note that the output list will have the most recent input first.

addInputs : Accumulator number number

An accumulator which adds numeric inputs.

appendInputToOutput : Accumulator appendable appendable

An accmulator which appends the input to the output so far.

appendOutputToInput : Accumulator appendable appendable

An accumulator which appends the output to far to the input.

Running a debouncer


type Msg i

Messages which the debouncer handles.

You will need to integrate this into your own Msg type, and then handle it in your update function (see code example above).

The type parameter represents the type of the input to be provided to the debouncer. Thus, it should match the i in Debouncer i o.

You can construct a message with provideInput, emitNow, cancelNow, or settleNow.

The only message you will typically need to send to the debouncer explicitly is the message that provides input. You can construct such a message with the provideInput function. Other messages are used internally by the debouncer.

provideInput : i -> Msg i

Construct a message that provides input to a debouncer.

The type parameter represents the type of the input to be provided. One typical example would be your own Msg type, but it can be anything you like. It will need to match the i in your Debouncer i o type.

emitNow : Msg i

Construct a message which will emit any accumulated output. This doesn't affect whether the debouncer is "settled" or not. If you'd like to emit output and force the debouncer to be settled, then use settleNow instead.

settleNow : Msg i

Construct a message which settles the debouncer now, even if it wouldn't otherwise settle at this time.

Any accumulated output will be emitted. If you want to settle without emitting any output, use cancel or cancelNow instead.

cancel : Debouncer i o -> Debouncer i o

Cancel any input collected so far (and not yet emitted). This throws away whatever input has been provided in the past, and forces the debouncer back to a "settled" state (without emitting anything).

cancelNow : Msg i

Like cancel, but operates via a message instead of acting directly on the debouncer. This is a convenience for cases where you'd like to cancel via a message.

update : Msg i -> Debouncer i o -> ( Debouncer i o, Platform.Cmd.Cmd (Msg i), Maybe o )

Handle a message for the debouncer.

You will need to integrate this into your update function, so that the debouncer can act on its messages. (There is a code example at the top of the module docs).

If we have arrived at a time for output to be emitted, then the extra return parameter will be the output. So, you should do whatever it is you want to do with that output. For instance, if the output is your Msg type, you should recursively feed it into your update function. (Again, see the example above for that case). But that is just one example -- the output type can be whatever you wish, and you can decide what to do with it.