lue-bird/elm-state-interface-experimental/13.0.0

Web

A state-interface program that can run in the browser

{ initialState : state
, interface : state -> Interface state
, ports :
{ toJs : Value -> Cmd (ProgramEvent state)
, fromJs : (Value -> ProgramEvent state) -> Sub (ProgramEvent state)
}
}
-> Program state

Create a Program:

  • The state is everything the program knows (what The Elm Architecture calls model). And it always starts with a given initialState

  • The Interface is the face to the outside world and can be created using the helpers in time, DOM, HTTP etc. The given interface function constructs these interfaces based on the current state

  • Connections to and from js

    port toJs : Json.Encode.Value -> Cmd event_
    
    
    port fromJs : (Json.Encode.Value -> event) -> Sub event
type alias Program state =
Program () (ProgramState state) (ProgramEvent state)

A Platform.Program that elm can run, produced by Web.program

You can also embed a state-interface program as part of an existing app that uses The Elm Architecture.

interface

type alias Interface future =
Dict String (InterfaceSingle future)

Incoming and outgoing effects. To create one, use the helpers in time, DOM, HTTP etc.

To combine multiple, use Web.interfaceBatch and Web.interfaceNone. To change the value that comes back in the future, use Web.interfaceFutureMap

interfaceBatch : List (Interface future) -> Interface future

Combine multiple Interfaces into one

interfaceNone : Interface future_

Doing nothing as an Interface. These two examples are equivalent:

Web.interfaceBatch [ a, Web.interfaceNone, b ]

and

Web.interfaceBatch
    (List.filterMap identity
        [ a |> Just, Nothing, b |> Just ]
    )
(future -> mappedFuture)
-> Interface future
-> Interface mappedFuture

Take what the Interface can come back with and return a different future value.

In practice, this is sometimes used like a kind of event-config pattern:

Web.timePosixRequest
    |> Web.interfaceFutureMap (\timeNow -> TimeReceived timeNow)


button "show all entries"
    |> Web.domRender
    |> Web.interfaceFutureMap (\Pressed -> ShowAllEntriesButtonClicked)

sometimes as a way to deal with all events (like update in The Elm Architecture)

...
    |> Web.interfaceFutureMap
        (\event ->
            case event of
                MouseMovedTo newMousePoint ->
                    { state | mousePoint = newMousePoint }


                CounterDecreaseClicked ->
                    { state | counter = state.counter - 1 }


                CounterIncreaseClicked ->
                    { state | counter = state.counter + 1 }
        )

and sometimes to nest events (like Cmd.map/Task.map/Sub.map/... in The Elm Architecture):

type Event
    = DirectoryTreeViewEvent TreeUiEvent
    | SortButtonClicked


type TreeUiEvent
    = Expanded TreePath
    | Collapsed TreePath


interface : State -> Interface State
interface state =
    ...
        [ treeUi ..
            |> Web.interfaceFutureMap DirectoryTreeViewEvent
        , ...
        ]
        |> Web.domRender


treeUi : ... -> Web.DomNode TreeUiEvent

In all these examples, you end up converting the narrow future representation of part of the interface to a broader representation for the parent interface

time

See elm/time

timePosixRequest : Interface Posix

An Interface for getting the current POSIX time.

Replacement for elm/time's Time.now.

timeZoneRequest : Interface Zone

An Interface for getting a Time.Zone based on the current UTC offset.

Replacement for elm/time's Time.here.

timeZoneNameRequest : Interface String

Intended for package authors. An Interface for using Intl.DateTimeFormat().resolvedOptions().timeZone to get names like Europe/Moscow or America/Havana. From there you can look it up in any IANA data you loaded yourself.

Replacement for elm/time's Time.getZoneName.

timePeriodicallyListen : Duration -> Interface Posix

An Interface for getting the current time every time a given Duration has passed.

Note: Do not use it for animations. Web.animationFrameListen syncs up with repaints and will end up being much smoother for any moving visuals.

timeOnceAt : Posix -> Interface Posix

An Interface for getting a reminder once a given point in time has been reached.

This lets you for example wait until it's 15 minutes before the event, timeout a request or schedule a certain action to a specific time.

import Web


type RequestState result
    = NotAsked
    | BeforeTimeout { start : Time.Posix }
    | TimedOut
    | GotResult result


{ initialState = NotAsked
, interface =
    \state ->
        [ case state of
            NotAsked ->
                [ Web.timePosixRequest
                    |> Web.interfaceFutureMap BeforeTimeout
                , ..request.. |> Web.futureMap GotResult
                ]


            BeforeTimeout requestTime ->
                -- timeout after 10 seconds
                Web.timeOnceAt (Duration.addTo requestTime (Duration.seconds 10))
                    |> Web.interfaceFutureMap (\_ -> TimedOut)


            TimedOut ->
                ...


            GotResult result ->
                ...
        ]
}

You can abstract this in various ways like adding a

withTimeout :
    ..Request result..
    -> Web.Interface (RequestState result)

where the result can be put into the "main state" and therefore cased on.

DOM

Primitives used for SVG and HTML (filling the same role as elm/virtual-dom)

domText : String -> DomNode future_

Plain text DOM node

String
-> List (DomModifier future)
-> List (DomNode future)
-> DomNode future

Create a DOM element with a given tag, DomModifiers and sub-nodes. For example to get <p>flying</p>

Web.domElement "p"
    []
    [ Web.domText "flying" ]

To create an SVG element, use Web.svgElement

String
-> List (DomModifier future)
-> List (DomNode future)
-> DomNode future

Create an SVG DOM node. with a given tag, DomModifiers and sub-nodes

String
-> String
-> List (DomModifier future)
-> List (DomNode future)
-> DomNode future

Create a DOM element with a given namespace, tag, DomModifiers and sub-nodes. For example, Web.svgElement is defined as

svgElement : String -> List (Modifier future) -> List (DomNode future) -> DomNode future
svgElement tag modifiers subs =
    Web.domElementNamespaced "http://www.w3.org/2000/svg" tag modifiers subs
type DomNode future
= DomText String
| DomElement (DomElement future)

Plain text or an DomElement. Create using domText and domElement

domFutureMap : (future -> mappedFuture) -> DomNode future -> DomNode mappedFuture

Wire events from this DOM node to a specific event, for example

buttonUi "start"
    |> Web.domFutureMap (\Clicked -> StartButtonClicked)

with

buttonUi : String -> Web.DomNode ButtonEvent
buttonUi label =
    Web.domElement "button"
        [ Web.domListenTo "click"
            |> Web.domModifierFutureMap (\_ -> Clicked)
        ]
        [ Web.domText label ]


type ButtonEvent
    = Clicked
domRender : DomNode future -> Interface future

An Interface for displaying a given DOM node

type alias DomModifier future =
Rope (DomModifierSingle future)

Setting of a Web.DomElement. To create one, use domAttribute, domStyle, domListenTo etc. To combine multiple, use Web.domModifierBatch and Web.domModifierNone

For example to get <a href="https://elm-lang.org">elm</a>

Web.domElement "a"
    [ Web.domAttribute "href" "https://elm-lang.org" ]
    [ Web.domText "elm" ]

Btw: If you can think of a nicer name for this like "customization", "characteristic" or "aspect", please open an issue.

attribute vs property

  • attribute: part of the HTML itself. E.g. class in <div class="greeting"></div>
  • property: an actual field on that js DOM object. E.g. className in div.className = "greeting"

But don't be surprised: There are cases where

  • only a property or an attribute of a name exists
  • both exist but they have different effects

For example, trying to reset the text inside a a text input with

Web.domAttribute "value" "user input"


-- then later replace it with
Web.domAttribute "value" ""

will only provide a default value and has no effect on the currently written text, so you'll have to use

Web.domStringProperty "value" ""

Similarly for checkboxes:

Web.domBoolProperty "checked" False

Maybe a rule of thumb is: Use properties to set anything related to interactivity and attributes for everything else.

If you have some opinions or better explanations, please open an issue.

(future -> mappedFuture)
-> DomModifier future
-> DomModifier mappedFuture

Wire events from this DomModifier to a specific event.

Web.domListenTo "click" |> Web.domModifierFutureMap (\_ -> ButtonClicked)
domModifierBatch : List (DomModifier future) -> DomModifier future

Combine multiple DomModifiers into one.

domModifierNone : DomModifier future_

Doing nothing as a DomModifier. These two examples are equivalent:

Web.domModifierBatch
    [ a, Web.domModifierNone, b ]

and

Web.domModifierBatch
    (List.filterMap identity
        [ a |> Just, Nothing, b |> Just ]
    )
domAttribute : String -> String -> DomModifier future_

A key-value attribute DomModifier

domAttributeNamespaced : String -> String -> String -> DomModifier future_

A namespaced key-value attribute DomModifier. For example, you could define an SVG xlink href attribute as

attributeXlinkHref : String -> Web.DomModifier msg
attributeXlinkHref value =
    Web.domAttributeNamespaced "http://www.w3.org/1999/xlink" "xlink:href" value
domStyle : String -> String -> DomModifier future_

A key-value style DomModifier

domBoolProperty : String -> Bool -> DomModifier future_

A key-bool value DOM property DomModifier

domStringProperty : String -> String -> DomModifier future_

A key-string value DOM property DomModifier

domListenTo : String -> DomModifier Value

Listen for a specific DOM event on the Web.DomElement. Use domModifierFutureMap to wire this to a specific event.

If you want to override the browser's default behavior for that event, use domListenToPreventingDefaultAction.

Be aware that unlike default js behavior, this event will not be passed further to parent dom elements (instead, it "stops propagation"/"doesn't bubble"). For example, if you have an element on top of some background frame and both listen to a click event, only the button will be notified.

If you for reasons can't go without bubbling, please tell me about it in an issue so I can make it more customizable or find alternatives.

domListenToPreventingDefaultAction : String -> DomModifier Value

Like domListenTo but preventing the browser's default action.

That's for example how elm's Browser.Events.onSubmit prevents the form from changing the page’s location:

submitListen : Web.DomModifier ()
submitListen =
    Web.domListenToPreventingDefaultAction "submit"
        |> Web.domModifierFutureMap (\_ -> ())

Ensure a given initial DomElementVisibilityAlignment in both directions.

Unlike domStyles, this is just an initial configuration which can be changed by user actions. So adding e.g. scrollToShow { y = Web.DomElementStart, x = Web.DomElementStart } will scroll to the top left once the next render happens but will not prevent users from scrolling away.

Note: Uses Element.scrollIntoView

= DomElementStart
| DomElementEnd
| DomElementCenter

What part of the Web.domElement should be visible

  • DomElementStart: mostly for text to read
  • DomElementEnd: mostly for text to write
  • DomElementCenter: mostly for images
DomModifier
{ fromLeft : Float
, fromTop : Float
}

Getting the current scroll position from the left and top.

Use in combination with domScrollToPosition to implement saving and restoring scroll position even when users had navigated off a URL.

{ fromLeft : Float
, fromTop : Float
}
-> DomModifier future_

Ensure a given initial scroll position in both directions. To move to the edge in a direction, use domScrollToShow instead.

Unlike domStyles, this is just an initial configuration which can be changed by user actions. So adding e.g. scrollToPosition ... will scroll once the next render happens but will not prevent users from scrolling away.

window

Observe and alter the page's global environment

animationFrameListen : Interface Posix

An Interface for detecting when animation frames occur. This will be about 60 times per second, though 75, 120, and 144 are also widely used. To balance this out in your animation, the current time is provided each frame.

To get a delta, you could use Web.timePosixRequest to get a start time and check with e.g. Duration.from how far you've progressed in the timeline.

Note: To improve performance and battery life, most browsers pause these notifications when the app is running in a background tab or a hidden <iframe>.

Replacement for Browser.Events.onAnimationFrame

Note: uses window.requestAnimationFrame.

An Interface for detecting changes to the visibility to the user

You can use times where the page becomes hidden to for example pause a currently running game. These times will also be the last reliable observation you can make before a user might close the page, so treat it as the likely end of the user's session

= WindowShown
| WindowHidden

The visibility to the user

  • WindowHidden: the user has navigated to a new page, switched tabs, closed the tab, minimized or closed the browser, or, on mobile, switched from the browser to a different app
  • WindowShown otherwise
Interface
{ width : Int
, height : Int
}

An Interface for getting the inner window width and height in pixels, not including toolbars/scrollbars

Interface
{ width : Int
, height : Int
}

An Interface for detecting changes to the inner window width and height

preferredLanguagesRequest : Interface (List String)

An Interface for reading the languages the user prefers. Each described using language tags according to RFC 5646: Tags for Identifying Languages (also known as BCP 47). In the returned list they are ordered by preference with the most preferred language first.

Note: uses window.navigator.languages

preferredLanguagesChangeListen : Interface (List String)

An Interface for detecting changes to the languages the user prefers. Each described using language tags according to RFC 5646: Tags for Identifying Languages (also known as BCP 47). In the returned list they are ordered by preference with the most preferred language first.

Note: uses window.onlanguagechange

documentListenTo : String -> Interface Value

An Interface for detecting a specific document event that has no native Interface, like like scroll, scrollend, selectionchange or paste

windowListenTo : String -> Interface Value

An Interface for detecting a specific window event

mediaQueryRequest : String -> Interface Bool

An Interface for checking if a given CSS @media query matches. For example

Web.mediaQueryRequest "(prefers-color-scheme: dark)"
Web.mediaQueryRequest "(orientation: landscape)"
Web.mediaQuery "(prefers-reduced-motion) and (prefers-contrast: more)"
Web.mediaQueryRequest "(100px < width < 700px)"

Note: uses window.matchMedia(_).matches

mediaQueryChangeListen : String -> Interface Bool

An Interface for watching for when a given CSS @media query starts to match and stops to match.

Web.mediaQueryRequest "(prefers-color-scheme: dark)"
    |> Web.futureMap
        (\enableDarkMode ->
            { state
                | configuredBackgroundColor =
                    if enableDarkMode then
                        Color.black


                    else
                        Color.white
            }
        )

Note: listens to the window.matchMedia(_).*Listener(...)

When navigating to a new page on the same site, you may want to change the document's context:

titleReplaceBy : String -> Interface future_

An Interface for setting the document's title

authorSet : String -> Interface future_

An Interface for adding or replacing the document's author metadata

keywordsSet : List String -> Interface future_

An Interface for adding or replacing the document's keywords metadata which should consist of words relevant to the page's content

descriptionSet : String -> Interface future_

An Interface for adding or replacing the document's description metadata which should be a short and accurate summary of the content of the page. Several browsers, like Firefox and Opera, use this as the default description of bookmarked pages.

history interaction

urlRequest : Interface AppUrl

An Interface for getting the current page's app-specific URL. Is usually used while starting up the app.

Note: Uses window.location.href

urlPush : AppUrl -> Interface future_

An Interface for changing the app-specific URL and adding a new entry to the browser history without triggering a page load.

Replacement for Browser.Navigation.urlPush

urlReplace : AppUrl -> Interface future_

An Interface for changing the app-specific URL, without triggering a page load or adding a new entry to the browser history.

This can be useful if you have search box and you want the ?search=hats in the URL to match without adding a history entry for every single key stroke. Imagine how annoying it would be to click back thirty times and still be on the same page!

Replacement for Browser.Navigation.urlReplace

reload : Interface future_

An Interface for reloading the current page. This always results in a page load!

Note: This may grab resources from the browser cache.

Replacement for Browser.Navigation.reload

HTTP client

{ url : String
, method : String
, headers :
List
{ name : String
, value : String
}
, body : Maybe Bytes
}
-> Interface (Result HttpError Bytes)

An Interface for sending an HTTP request to a given url

exampleHttpPost : Web.Interface (Result Web.HttpError Bytes)
exampleHttpPost =
    Web.httpRequestSend
        { url = ...
        , method = "POST"
        , headers = [ { name = "Content-Type", value = "application/json" } ]
        , body =
            96
                |> Json.Encode.int
                |> Json.Encode.encode 0
                |> Bytes.Encode.encode
                |> Just
        }


exampleHttpGet : Web.HttpRequest
exampleHttpGet =
    Web.httpRequestSend
        { url = ...
        , method = "GET"
        , headers = [ { name = "X-Custom-Header", value = "ProcessThisImmediately" } ]
        , body = Nothing -- empty bytes with HEAD or GET can lead to an HttpError
        }
        |> Web.interfaceFutureMap
            (\responseOrError ->
                case responseOrError of
                    Ok bytes ->
                        case bytes |> Bytes.Decode.decode (Bytes.Decode.string (bytes |> Bytes.width)) of
                            Just jsonString ->
                                case jsonString |> Json.Decode.decodeString ... of
                                    Err jsonError ->
                                        Err (jsonError |> Json.Decode.errorToString)


                                    Ok parsed ->
                                        Ok parsed


                            Nothing ->
                                Err "failed to decode bytes to a string"


                    Err _ ->
                        Err "HTTP response error"
            )

Usually, if you have a request body, the headers include Content-Type with a MIME type

The response will either contain Bytes. or an HttpError

If you for some reason need a timeout, add a Web.timeOnceAt and remove the Web.httpRequestSend once it has passed.

Uses the fetch API

= HttpBadUrl
| HttpNetworkError String
| HttpBadStatus
{ statusCode : Int
, statusText : String
, headers :
List
{ name : String
, value : String
}
, body : Bytes
}

A Request can fail in a couple ways:

  • BadUrl: you did not provide a valid URL
  • NetworkError: the user turned off their wifi, went in a cave, etc. or the server CORS is misconfigured. Note: A 404 for example does not constitute a network error
  • BadStatus: you got a response back, but the status code indicates failure. Contains:
    • the statusCode like 404 and statusText describing what the statusCode means a little
    • headers like Content-Length and Expires
    • the body as Bytes

random

Not familiar with random "generators"? elm/random explains it nicely!

Here's an example showing a number between 1 and 6 and a button to reroll using NoRedInk/elm-random-pcg-extended

import Random.Pcg.Extended
import Web


type State
    = WaitingForInitialRandomness
    | DiceUiState { diceEyes : Int, seed : Random.Pcg.Extended.Seed }


type DiceUiEvent
    = RerollClicked


diceEyesRandomGenerator : Random.Pcg.Extended.Generator Int
diceEyesRandomGenerator =
    Random.Pcg.Extended.int 1 6


{ initialState = WaitingForInitialRandomness
, interface =
    \state ->
        case state of
            WaitingForInitialRandomness ->
                Web.randomUnsignedInt32sRequest 4
                    |> Web.interfaceFutureMap
                        (\unsignedInt32s ->
                            let
                                initialSeed : Random.Pcg.Extended.Seed
                                initialSeed =
                                    Random.Pcg.Extended.initialSeed (unsignedInt32s |> List.head |> Maybe.withDefault 0) (unsignedInt32s |> List.drop 1)


                                ( diceEyes, newSeed ) =
                                    Random.Pcg.Extended.step diceEyesRandomGenerator initialSeed
                            in
                            DiceUiState { diceEyes = diceEyes, seed = newSeed }
                        )


            DiceUiState randomStuff ->
                Web.domElement "div"
                    []
                    [ randomStuff.diceEyes |> String.fromInt |> Web.domText
                    , Web.domElement "button"
                        [ Web.domListenTo "click"
                            |> Web.domModifierFutureMap (\_ -> RerollClicked)
                        ]
                        [ Web.domText "roll the dice" ]
                    ]
                    |> Web.domRender
                    |> Web.interfaceFutureMap
                        (\RerollClicked ->
                            let
                                ( diceEyes, newSeed ) =
                                    Random.Pcg.Extended.step diceEyesRandomGenerator randomStuff.seed
                            in
                            DiceUiState { diceEyes = diceEyes, seed = newSeed }
                        )
}
randomUnsignedInt32sRequest : Int -> Interface (List Int)

An Interface for generating a given count of cryptographically sound unsigned 32-bit Ints. You can use these in all kinds of packages that allow creating an initial seed from ints like NoRedInk/elm-random-pcg-extended

Note: uses window.crypto.getRandomValues

console

consoleLog : String -> Interface future_

An Interface for printing a message with general information like if certain tasks have been successful

survey submitted and received successfully

Depending on what minifying tools you use for your production build, these might get removed.

Note: uses console.log, just like Debug.log

consoleWarn : String -> Interface future_

An Interface for printing a message that something didn't succeed but you could recover from, for example

⚠️ Unknown device - there may be compatibility issues.

⚠️ Recoverable upload failure, will retry. Error was: no status.

Note: uses console.warn

consoleError : String -> Interface future_

An Interface for printing a message that something failed with bad consequences, for example

❌ decoding the selected file failed. Please report this bug at ...

Note: uses console.error

local storage

Saved data for the url origin (protocol, host name, port) across browser sessions.

This data doesn't expire and won't be cleared when the page is closed. The only exception is "incognito mode", where all data is cleared once the last "private" tab is closed.

see mdn on Window.localStorage

localStorageRequest : String -> Interface (Maybe String)

An Interface for reading the value of the entry with the given key. Comes back with Nothing if that key doesn't exist.

import Json.Decode
import Web


projectFromLocalStorageRequest : Web.Interface (Result String Project)
projectFromLocalStorageRequest =
    Web.localStorageRequest "project"
        |> Web.interfaceFutureMap
            (\savedProject ->
                case savedProject of
                    Nothing ->
                        "nothing had been saved" |> Err


                    Just savedProjectJsonString ->
                        savedProjectJsonString
                            |> Json.Decode.decodeString projectJsonDecoder
                            |> Result.mapError Json.Decode.errorToString
            )
localStorageSet : String -> String -> Interface future_

An Interface for replacing the value of the entry with the given key or adding key and value as a new entry if the key doesn't exist.

import Json.Decode
import Web


projectSaveToLocalStorage : Project -> Web.Interface future_
projectSaveToLocalStorage =
    \project ->
        Web.localStorageSet "project"
            (project |> projectJsonEncode |> Json.Encode.encode 0)

Note: This will trigger an event for all other tabs of the same url origin that can be listened to using localStorageSetOnADifferentTabListen

localStorageRemove : String -> Interface future_

An Interface for deleting the entry with the given key if it exists.

Note: This will trigger an event for all other tabs of the same url origin that can be listened to using localStorageRemoveOnADifferentTabListen

String
-> Interface
{ appUrl : AppUrl
, oldValue : Maybe String
, newValue : String
}

An Interface for keeping an eye on when the local storage on a different tab with the same url origin is set.

When the oldValue is Nothing, no entry with that key existed.

localStorageRemoveOnADifferentTabListen : String -> Interface AppUrl

An Interface for keeping an eye on when the local storage on a different tab with the same url origin is removed.

web sockets

= SocketOpened
| SocketDataReceived String
| SocketClosed
{ code : Int
, reason : String
}

How the connection has changed or what message has been sent

{ address : String
, dataToSend : Maybe String
}
-> Interface SocketEvent

An Interface for detecting when data has been sent from the server at a given address or the connection has changed, see Web.SocketEvent.

You also have the ability to send data to the server whenever necessary. It's common to pair this with Json.Encode.encode 0 to send json.

case state.answer of
    Nothing ->
        Web.socketListenAndMaybeSend
            { address = "ws://127.0.0.1:9000"
            , dataToSend = Just "Are you still on?"
            }
            |> Web.interfaceFutureMap
                (\socketEvent ->
                    case socketEvent of
                        Web.SocketDataReceived answer ->
                            { state | answer = Just answer }


                        Web.SocketOpened ->
                            state


                        Web.SocketClosed _ ->
                            state
                )


    Just _ ->
        Web.domRender (Web.domText "Just wanted to hear your voice, thx bye!")

"Removing" Web.socketListenAndMaybeSend from your interface will close the connection.

file download

Downloading a dynamically generated file.

Security note: Browsers require downloads to be initiated by a user event. So rather than allowing malicious sites to put files on your computer however they please, the user has to at least click a button first. As a result, the following interfaces only work when they are triggered by some user event.

Note: There's no equivalent module for file select since you can easily replicate the behavior using an input element with type file or file drop area modifiers, see for example mpizenberg/elm-file.

{ name : String
, mimeType : String
, content : Bytes
}
-> Interface future_

An Interface for downloading a given file with Bytes as its content, a given type and and a given default name.

Replacement for File.Download.bytes

audio

Play sounds and music.

import Time
import Web


type alias State =
    { audioSource : Maybe (Result Web.AudioSourceLoadError Web.AudioSource)
    , audioStartTime : Maybe Time.Posix
    }


{ initialState =
    { audioSource = Nothing
    , audioStartTime = Nothing
    }
, interface =
    \state ->
        case state.audioSource of
            Just (Ok audioSource) ->
                case state.audioStartTime of
                    Just startTime
                        Web.audioFromSource audioSource startTime
                            |> Web.audioPlay




                    Nothing ->
                        Web.timePosixRequest
                            |> Web.interfaceFutureMap
                                (\time -> { state | audioSTartTime = time |> Just })


            Nothing ->
                Web.audioSourceLoad "https://s3-us-west-2.amazonaws.com/s.cdpn.io/123941/Yodel_Sound_Effect.mp3"
                    |> Web.interfaceFutureMap
                        (\result -> { state | audioSource = result |> Just })


            Just (Err _) ->
                Web.consoleError "audio failed to load"
}
type alias Audio =
RecordWithoutConstructorFunction
{ url : String
, startTime : Posix
, volume : AudioParameterTimeline
, speed : AudioParameterTimeline
, stereoPan : AudioParameterTimeline
, processingLastToFirst : List AudioProcessing
}

Some kind of sound we want to play. To create Audio, start with Web.audioFromSource

type alias AudioSource =
RecordWithoutConstructorFunction
{ url : String
, duration : Duration
}

Audio data we can use to play sounds. Use Web.audioSourceLoad to fetch an AudioSource.

You can also use the contained source duration, for example to find fade-out times or to create a loop:

audioLoop : AudioSource -> Time.Posix -> Time.Posix -> Audio
audioLoop source initialTime lastTick =
    Web.audioFromSource source
        (Duration.addTo
            initialTime
            (source.duration
                |> Quantity.multiplyBy
                    (((Duration.from initialTime lastTick |> Duration.inSeconds)
                        / (source.duration |> Duration.inSeconds)
                     )
                        |> floor
                        |> toFloat
                    )
            )
        )
= AudioSourceLoadDecodeError
| AudioSourceLoadNetworkError

These are possible errors we can get when loading an audio source file.

  • AudioSourceLoadDecodeError: This means we got the data but we couldn't decode it. One likely reason for this is that your url points to the wrong place and you're trying to decode a 404 page instead.
  • AudioSourceLoadNetworkError: We couldn't reach the url. Either it's some kind of CORS issue, the server is down, or you're disconnected from the internet.
= AudioLinearConvolution { sourceUrl : String }
| AudioLowpass { cutoffFrequency : AudioParameterTimeline }
| AudioHighpass { cutoffFrequency : AudioParameterTimeline }

A single effect filter applied to an Audio

{ startValue : Float
, keyFrames :
List
{ time : Posix
, value : Float
}
}

defining how loud a sound should be at any point in time

audioSourceLoad : String -> Interface (Result AudioSourceLoadError AudioSource)

An Interface for fetching audio data from a given url and returning an AudioSource to use with audioFromSource.

audioFromSource : AudioSource -> Posix -> Audio

Create Audio from an given loaded source which will play at a given time

-- play a song at half speed and wait 2 seconds after the usual song start time before starting
Web.audioFromSource
    myCoolSong
    (Duration.addTo usualSongStartTime (Duration.seconds 2))
    |> Web.audioSpeedScaleBy (Web.audioParameterAt 0.5)

Note that in some browsers audio will be muted until the user interacts with the webpage.

audioPlay : Audio -> Interface future_

An Interface for playing Audio created with audioFromSource.

To play multiple audios:

[ audio0, audio1, audio2 ]
    |> List.map Web.audioPlay
    |> Web.interfaceBatch
audioVolumeScaleBy : AudioParameterTimeline -> Audio -> Audio

Scale how loud it is. 1 preserves the current volume, 0.5 halves it, and 0 mutes it. If the the volume is less than 0, 0 will be used instead.

audioSpeedScaleBy : AudioParameterTimeline -> Audio -> Audio

Scale the playback rate by a given factor. This will also affect pitch.

For example, Web.audioSpeedScaleBy 0.5 means playback will take twice as long and the pitch will be one octave lower, see AudioBufferSourceNode.playbackRate

In general, to pitch by semitones:

Web.audioSpeedScaleBy
    (Web.audioParameterAt (2 ^ (semitones / 12)))

Note: It would be possible to modify the signal to compensate for the pitch change, see Audio time stretching and pitch scaling. Help appreciated!

audioStereoPan : AudioParameterTimeline -> Audio -> Audio

Change the stereo panning with a given a signed percentage parameter.

Web.audioStereoPan -0.9 for example means that the sound is almost fully balanced towards the left speaker

audioAddLinearConvolutionWith : AudioSource -> Audio -> Audio

Usually used to apply reverb and or echo. Given a loaded AudioSource containing the impulse response, it performs a Convolution with the Audio

If you need some nice impulse wavs to try it out, there's a few at dhiogoboza/audio-convolution. If you know more nice ones, don't hesitate to open an issue or a PR.

audioAddHighpassFromFrequency : AudioParameterTimeline -> Audio -> Audio

Frequencies below a given cutoff parameter are attenuated; frequencies above it pass through.

Has a 12dB/octave rolloff and no peak at the cutoff.

audioAddLowpassUntilFrequency : AudioParameterTimeline -> Audio -> Audio

Frequencies below a given cutoff parameter pass through; frequencies above it are attenuated.

Has a 12dB/octave rolloff and no peak at the cutoff.

To detune, use audioSpeedScaleBy. It's documentation also shows which scale relates to which semitone pitch.

To build an AudioParameterTimeline:

audioParameterAt : Float -> AudioParameterTimeline

Set it to a constant value. Add audioParameterThrough to make it transition from this starting value over time

audioParameterThrough : Posix -> Float -> AudioParameterTimeline -> AudioParameterTimeline

Specify a key value at a given absolute point in time. The parameter will then transition linearly between those points.

Let's define an audio function that fades in to 1 and then fades out until it's 0 again.

import Duration
import Time
import Web




-- 1                ________
--                /         \
-- 0 ____________/           \_______
--    t ->    fade in     fade out
fadeInOut fadeInStartTime fadeOutEndTime audio =
    Web.audioParameterAt 0
        |> Web.audioParameterThrough fadeInStartTime 1
        |> Web.audioParameterThrough (Duration.addTo fadeInStartTime Duration.second) 1
        |> Web.audioParameterThrough (Duration.subtractFrom fadeOutEndTime Duration.second) 1
        |> Web.audioParameterThrough fadeOutEndTime 0

You do not have to worry about order.

clipboard

clipboardRequest : Interface String

An Interface for reading the textual contents of the system clipboard.

Note: uses navigator.clipboard.readText

clipboardReplaceBy : String -> Interface future_

An Interface for setting the textual contents of the system clipboard.

Note: uses navigator.clipboard.writeText

Note: To listen for copy, cut and paste events, use Web.domListenTo

geo location

Observe the GeoLocation using the web geolocation API.

type alias GeoLocation =
RecordWithoutConstructorFunction
{ latitudeInDecimalDegrees : Float
, longitudeInDecimalDegrees : Float
, latitudeLongitudeAccuracy : Maybe Length
, altitudeAboveNominalSeaLevel : Maybe Length
, altitudeAccuracy : Maybe Length
, headingWith0AsTrueNorthAndIncreasingClockwise : Maybe Angle
, speed : Maybe Speed
}

Position and (if available) altitude of the device on Earth, as well as the accuracy with which these properties are calculated. The geographic position information is provided in terms of World Geodetic System coordinates (WGS84).

Device movement direction and speed might also be provided.

Length, Angle and Speed are from ianmackenzie/elm-units

geoLocationRequest : Interface GeoLocation

An Interface for getting the current position of the device

geoLocationChangeListen : Interface GeoLocation

An Interface for detecting changes in the current position of the device

gamepads

Observe connected gamepads and other game controllers (not including motion sensing, gesture recognition etc.).

import Web


interface =
    \state ->
        [ Web.timePeriodicallyListen (Duration.seconds (1 / 50))
            |> Web.interfaceFutureMap
                ..simulate one tick using gamepad inputs..
        , [ Web.gamepadsRequest, Web.gamepadsChangeListen ]
            |> Web.interfaceBatch
            |> Web.interfaceFutureMap (\gamepads -> { state | gamepads = gamepads })
        ]
            |> Web.interfaceBatch

If your gamepad isn't showing up in the list, press some buttons. On some devices, only certain buttons will wake up the gamepad API (the shapes on PS3 controllers, for instance)

type alias Gamepad =
RecordWithoutConstructorFunction
{ primaryButton : GamepadButton
, secondaryButton : GamepadButton
, tertiaryButton : GamepadButton
, quaternaryButton : GamepadButton
, leftBumperButton : GamepadButton
, rightBumperButton : GamepadButton
, leftTriggerButton : GamepadButton
, rightTriggerButton : GamepadButton
, selectButton : GamepadButton
, startButton : GamepadButton
, leftThumbstickButton : GamepadButton
, rightThumbstickButton : GamepadButton
, upButton : GamepadButton
, downButton : GamepadButton
, leftButton : GamepadButton
, rightButton : GamepadButton
, homeButton : GamepadButton
, touchpadButton : GamepadButton
, additionalButtons : List GamepadButton
, kindId : String
, thumbstickLeft :
{ x : Float
, y : Float
}
, thumbstickRight :
{ x : Float
, y : Float
}
, thumbsticksAdditional :
List
{ x : Float
, y : Float
}
}

Controller information on button presses, thumbstick positions etc.

  • primaryButton: The most common action like "enter"/"confirm" or jump

  • secondaryButton: Usually "cancel"/"skip" or a common ability like duck/sneak, drop or heal

  • tertiaryButton: common-ish ability like consume, interact, reload or an ultimate power

  • quaternaryButton: less common action like quick menu or mode switch

  • leftBumperButton, rightBumperButton: The top row of smaller buttons on the side opposite of you, sometimes called shoulder buttons. Often used for alternative menu actions like slot switching or quick map

  • leftTriggerButton, rightTriggerButton: The bottom row of larger buttons on the side opposite of you. Often used for holdable abilities like sliding or dashing

  • selectButton: Usually opens an in-world menu with for example inventory, lore, ability overview or a map

  • startButton: Usually pauses the game and opens a menu with options like settings and quit

  • leftThumbstickButton, rightThumbstickButton: Not all gamepads have these, so they often either double existing actions like "confirm" or perform actions that are only very rarely helpful, like hiding ui elements or making a screenshot

  • upButton, downBottom, leftButton, rightButton: exactly one step in a direction, usually in a (quick) menu/inventory

  • homeButton: Usually turns the gamepad on/off, or changes the state of the game

  • touchpadButton: Not present on most gamepads. While the touchpad is often used for controlling the mouse, it can also be used as a simple button

  • thumbstickLeft, thumbstickRight: Those wiggly that can be moved in any direction by any amount. They are provided as x, y signed percentages

  • kindId: some information about the gamepad, usually containing the USB vendor, product id of the gamepad and the name of the gamepad as provided by the driver. See mdn

    You can use this information to for example determine how to show controls

  • buttonsAdditional, thumbsticksAdditional: Maybe you have a weird gamepad with 3 thumbsticks? These might help 🤷

Implementation note: As you know, gamepad layouts differ between models. For most of them, we're able to map them to the buttons and thumbsticks above. If you experience issues with some model, open an issue

= GamepadButtonPressed { firmnessPercentage : Float }
| GamepadButtonReleased { isTouched : Bool }

Buttons are either held down with an optional value between 0 and 1 to measure how hard, or they are released with an optional detection of touch which defaults to false.

gamepadsRequest : Interface (Dict Int Gamepad)

An Interface for getting which gamepads are connected and in which ways buttons and axes are activated.

The given Dict keys uniquely identify each device for the whole session.

gamepadsChangeListen : Interface (Dict Int Gamepad)

An Interface for detecting changes to which gamepads are connected and in which ways buttons and axes are activated.

The given Dict keys uniquely identify each device for the whole session, so you can for example check for removed and added gamepads using Dict.diff or track one host device in the state.

Implementation note: The web gamepad API does not offer a listener to detect changes. So instead, we poll every 14ms (a bit more often than 60 times/second) which is just a bit faster than Chrome's actual fetch rate (fetch rate is not part of the specification).

We want to avoid missed inputs before your next simulation tick, so we just had to guess a good interval number. https://stackoverflow.com/a/51483510

If you have issues with unresponsiveness, open an issue

notification

Give important notices to the user as push notifications. Consider this a convenience feature, not something users have to rely upon. Always offer users alternative methods to view messages or initiate actions and allow users to opt out of getting more in the future.

notificationAskForPermission : Interface future_

Ask the user to consent to receiving notifications, if they haven't already.

For security reasons, browsers require some kind of user interaction like a button click first. So you could for example add a toggle to send notifications and ask only for permission when the toggle is set.

{ message : String
, details : String
, id : String
}
-> Interface NotificationClicked

An Interface for pushing a notification to the user. Users can in the future respond to it by clicking it.

To not notify users multiple times for multiple messages of the same kind, choose an id with a description for this kind so that the notification with the same kind gets silently replaced if it exists.

case messagesFromOpponent of
    [] ->
        Web.interfaceNone


    [ onlyMessage ] ->
        Web.notificationShow
            { id = "opponent messages"
            , message = "Your opponent left a message"
            , details = "\"" ++ onlyMessage ++ "\""
            }


    _ :: _ :: message2Up ->
        Web.notificationShow
            { id = "opponent messages"
            , message =
                [ "Your opponent left "
                , 2 + (message2Up |> List.length) |> String.fromInt
                , " messages"
                ]
                    |> String.join
            , details = ""
            }

Another example of a reminder shown in advance

let
    untilMeeting : Duration
    untilMeeting =
        Duration.from meetingTime currentTime
in
if untilMeeting |> Quantity.isLessThan (Duration.minutes 10) then
    Web.notificationShow
        { id = "time until meeting"
        , message =
            [ "the meeting starts in "
            , untilMeeting |> Duration.inMinutes |> Basics.ceiling |> String.fromInt)
            , " minutes"
            ]
                |> String.concat
        , details = ""
        }


else
    Web.interfaceNone

Note: If you haven't previously used notificationAskForPermission it will request this permission now.

= NotificationClicked

The user clicked a displayed notification, moving the focus to our page

You can combine it with Web.windowVisibilityChangeListen to only notify users when they're on a different page

import Web


type State
    = State
        { windowVisibility : Web.WindowVisibility
        , whoseMove : Player
        , mode : Mode
        , notificationPermissionToggle : Permission
        }


type Permission
    = Rejected
    | Accepted


type Mode
    = LongGameBoardMode
    | SettingsPage


type Player
    = You
    | Opponent


interface : State -> Web.Interface State
interface =
    \(State state) ->
        [ case state.notificationPermissionToggle of
            Accepted ->
                Web.notificationAskForPermission


            Rejected ->
                Web.interfaceNone
        , case state.windowVisibility of
            Web.WindowShown ->
                Web.interfaceNone


            Web.WindowHidden ->
                case state.whoseTurn of
                    Opponent ->
                        Web.interfaceNone


                    You ->
                        Web.Notification.show
                            { message = "opponent moved", ... }
                            |> Web.interfaceFutureMap
                                (\Web.NotificationClicked ->
                                    -- return to the game if previously in settings
                                    State { state | mode = LongGameBoardMode }
                                )


        , case state.mode of
            LongGameBoardMode ->
                ..listen for opponent move from server..
                    |> Web.interfaceFutureMap
                        (\... -> State { state | whoseMove = You })


            SettingsPage ->
                ..toggle for accepting/rejecting notifications..
                    |> Web.interfaceFutureMap
                        (\... -> State { state | notificationPermissionToggle = ..opposite.. })
        ]
            |> Web.interfaceBatch

embed

If you just want to replace a part of your elm app with this architecture. Make sure to wire in all 3:

type alias ProgramConfig state =
RecordWithoutConstructorFunction
{ initialState : state
, interface : state -> Interface state
, ports :
{ toJs : Value -> Cmd (ProgramEvent state)
, fromJs : (Value -> ProgramEvent state) -> Sub (ProgramEvent state)
}
}

What's needed to create a state-interface program

ProgramConfig state
-> ( ProgramState state, Cmd (ProgramEvent state) )

The "init" part for an embedded program

ProgramConfig state
-> ProgramEvent state
-> ProgramState state
-> ( ProgramState state, Cmd (ProgramEvent state) )

The "update" part for an embedded program

ProgramConfig state
-> ProgramState state
-> Sub (ProgramEvent state)

The "subscriptions" part for an embedded program

Under the hood, Web.program is then defined as just

program config =
    Platform.worker
        { init = \() -> Web.programInit yourAppConfig
        , update = Web.programUpdate yourAppConfig
        , subscriptions = Web.programSubscriptions yourAppConfig
        }

internals, safe to ignore for users

Exposed so can for example simulate it more easily in tests, add a debugger etc.

type alias DomElement future =
RecordWithoutConstructorFunction
{ header : DomElementHeader future
, subs : List (DomNode future)
}

A tagged DOM node that can itself contain child nodes

= DefaultActionPrevent
| DefaultActionExecute

Setting for a listen Web.DomModifier to keep or overwrite the browser's default action

type DomModifierSingle future
= Attribute
{ namespace : Maybe String
, key : String
, value : String
}
| StringProperty
{ key : String
, value : String
}
| BoolProperty
{ key : String
, value : Bool
}
| Style
{ key : String
, value : String
}
| ScrollToPosition
{ fromLeft : Float
, fromTop : Float
}
| ScrollToShow
}
| ScrollPositionRequest
( { fromLeft : Float
, fromTop : Float
}
-> future
)
| Listen
{ eventName : String
, on : Value -> future
, defaultActionHandling : DefaultActionHandling
}

An individual DomModifier. Create using domAttribute, domStyle, domListenTo etc.

type ProgramState appState
= State
{ interface : Interface appState
, appState : appState
}

The "model" in a Web.program

type ProgramEvent appState
= JsEventFailedToDecode Error
| JsEventCouldNotBeAssociated
| JsEventEnabledConstructionOfNewAppState appState

The "msg" in a Web.program

type InterfaceSingle future
= DocumentTitleReplaceBy String
| DocumentAuthorSet String
| DocumentKeywordsSet (List String)
| DocumentDescriptionSet String
| DocumentEventListen
{ eventName : String
, on : Decoder future
}
| ConsoleLog String
| ConsoleWarn String
| ConsoleError String
| NavigationReplaceUrl AppUrl
| NavigationPushUrl AppUrl
| NavigationGo Int
| NavigationLoad String
| NavigationReload ()
| NavigationUrlRequest (AppUrl -> future)
| FileDownload
{ mimeType : String
, name : String
, contentAsciiString : String
}
| ClipboardReplaceBy String
| ClipboardRequest (String -> future)
| AudioSourceLoad
{ url : String
, on : Result AudioSourceLoadError AudioSource -> future
}
| AudioPlay Audio
| DomNodeRender (DomNode future)
| NotificationAskForPermission ()
| NotificationShow
{ id : String
, message : String
, details : String
, on : NotificationClicked -> future
}
| HttpRequestSend
{ url : String
, method : String
, headers :
List
{ name : String
, value : String
}
, bodyAsciiString : Maybe String
, on : Result HttpError Bytes -> future
}
| TimePosixRequest (Posix -> future)
| TimezoneOffsetRequest (Int -> future)
| TimeOnce
{ pointInTime : Posix
, on : Posix -> future
}
| TimePeriodicallyListen
{ intervalDurationMilliSeconds : Int
, on : Posix -> future
}
| TimezoneNameRequest (String -> future)
| RandomUnsignedInt32sRequest
{ count : Int
, on : List Int -> future
}
| WindowSizeRequest
( { width : Int
, height : Int
}
-> future
)
| WindowPreferredLanguagesRequest (List String -> future)
| WindowEventListen
{ eventName : String
, on : Decoder future
}
| WindowVisibilityChangeListen (WindowVisibility -> future)
| WindowAnimationFrameListen (Posix -> future)
| WindowPreferredLanguagesChangeListen (List String -> future)
| MediaQueryRequest
{ queryString : String
, on : Bool -> future
}
| MediaQueryChangeListen
{ queryString : String
, on : Bool -> future
}
| SocketListen
{ address : String
, on : SocketEvent -> future
}
| SocketDataSend
{ address : String
, data : String
}
| LocalStorageSet
{ key : String
, value : Maybe String
}
| LocalStorageRequest
{ key : String
, on : Maybe String -> future
}
| LocalStorageRemoveOnADifferentTabListen
{ key : String
, on : AppUrl -> future
}
| LocalStorageSetOnADifferentTabListen
{ key : String
, on :
{ appUrl : AppUrl
, oldValue : Maybe String
, newValue : String
}
-> future
}
| GeoLocationRequest (GeoLocation -> future)
| GeoLocationChangeListen (GeoLocation -> future)
| GamepadsRequest (Dict Int Gamepad -> future)
| GamepadsChangeListen (Dict Int Gamepad -> future)

A "non-batched" Interface. To create one, use the helpers in time, DOM, HTTP etc.

= DomHeaderText String
| DomElementHeader (DomElementHeader future)

Plain text or a DomElementHeader for use in an Interface.

type alias DomElementHeader future =
RecordWithoutConstructorFunction
{ namespace : Maybe String
, tag : String
, styles : SortedKeyValueList String String
, attributes : SortedKeyValueList String String
, attributesNamespaced :
{ namespace : String
, key : String
}
String
, stringProperties : SortedKeyValueList String String
, boolProperties : SortedKeyValueList String Bool
, scrollToPosition :
Maybe
{ fromLeft : Float
, fromTop : Float
}
, scrollToShow :
Maybe
}
, scrollPositionRequest :
Maybe
( { fromLeft : Float
, fromTop : Float
}
-> future
)
, eventListens :
String
{ on : Value -> future
, defaultActionHandling : DefaultActionHandling
}
}

Everything about a tagged DOM element except potential sub-nodes

type SortedKeyValueList key value
= SortedKeyValueList
(List
{ key : key
, value : value
}
)

Alternative to Dict optimized for fast merge and fast creation. Would be a terrible fit if we needed fast insert and get.

If you need more things like json encoders/decoders, open an issue