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

NOTE: the latest version is 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 =

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

elm/time primitives as part of an Interface.

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

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

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

= 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
= DefaultActionPrevent
| DefaultActionExecute

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

type DomNode future
= DomText String
| DomElement (DomElement future)

Plain text or an DomElement. Create using domText and domElement

domText : String -> DomNode future_

Plain text DomNode

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

A tagged DOM node that can itself contain child nodes

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 SVG elements, use Web.svgElement

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

Create an SVG element Web.DomNode. 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
domFutureMap : (future -> mappedFuture) -> DomNode future -> DomNode mappedFuture

Wire events from this Web.DomNode 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 Web.DomNode

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

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

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.

history interaction as part of an Interface

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

pushUrl : 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.pushUrl

replaceUrl : 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.replaceUrl

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

Helpers for HTTP requests as part of an Interface

type alias HttpRequest future =
RecordWithoutConstructorFunction
{ url : String
, method : String
, headers :
List
{ name : String
, value : String
}
, body : HttpBody
, expect : HttpExpect future
}

An HTTP request for use in an Interface.

Use Web.httpAddHeaders to set custom headers as needed. Use Web.timeOnceAt to add a timeout of how long you are willing to wait before giving up.

= HttpBodyEmpty
| HttpBodyString
{ mimeType : String
, content : String
}
| HttpBodyUnsignedInt8s
{ mimeType : String
, content : List Int
}

Data send in your http request.

  • HttpBodyEmpty: Create an empty body for your request. This is useful for GET requests and POST requests where you are not sending any data.

  • HttpBodyString: Put a String in the body of your request. Defining Web.httpJsonBody looks like this:

    import Json.Encode
    
    
    httpJsonBody : Json.Encode.Value -> Web.HttpBody
    httpJsonBody value =
        Web.HttpBodyString "application/json" (Json.Encode.encode 0 value)

    The first argument is a MIME type of the body.

  • HttpBodyUnsignedInt8s is pretty much the same as HttpBodyString but for Bytes, see Web.httpBodyBytes

type HttpExpect future
= HttpExpectString (Result HttpError String -> future)
| HttpExpectBytes (Result HttpError Bytes -> future)
| HttpExpectWhatever (Result HttpError () -> future)

Describe what you expect to be returned in an http response body.

= HttpBadUrl
| HttpNetworkError
| HttpBadStatus
{ metadata : HttpMetadata
, body : Value
}

A Request can fail in a couple ways:

  • BadUrl means you did not provide a valid URL
  • NetworkError means 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 means you got a response back, but the status code indicates failure. Contains:
    • The response Metadata.
    • The raw response body as a Json.Decode.Value.
type alias HttpMetadata =
RecordWithoutConstructorFunction
{ url : String
, statusCode : Int
, statusText : String
, headers : SortedKeyValueList String String
}

Extra information about the response:

  • url of the server that actually responded (so you can detect redirects)
  • statusCode like 200 or 404
  • statusText describing what the statusCode means a little
  • headers like Content-Length and Expires

Note: It is possible for a response to have the same header multiple times. In that case, all the values end up in a single entry in the headers. The values are separated by commas, following the rules outlined here.

httpRequest : HttpRequest future -> Interface future

An Interface for handling an HttpRequest using the fetch API

{ url : String
, expect : HttpExpect future
}
-> HttpRequest future

Create a GET HttpRequest.

Use Web.httpAddHeaders to set custom headers as needed. Use Web.timeOnceAt to add a timeout of how long you are willing to wait before giving up.

{ url : String
, body : HttpBody
, expect : HttpExpect future
}
-> HttpRequest future

Create a POST HttpRequest.

Use Web.httpAddHeaders to set custom headers as needed. Use Web.timeOnceAt to add a timeout of how long you are willing to wait before giving up.

httpAddHeaders : List ( String, String ) -> HttpRequest future -> HttpRequest future

Add custom headers to the Web.HttpRequest.

request
    |> Web.httpAddHeaders
        [ ( "X-Custom-Header", "ProcessThisImmediately" )
        ]

Expect the response body to be a String.

Decoder future
(Result
(Result
{ actualBody : String
, jsonError : Error
}
future
)
)

Expect the response body to be JSON, decode it using the given decoder. The result will either be

  • Err with an HttpError if it didn't succeed
  • Ok if there was a result with either

Expect the response body to be Bytes. The result will either be

  • Err with an HttpError if it didn't succeed
  • Ok with the Bytes

Discard the response body.

Put a given JSON value in the body of your request. This will automatically add the Content-Type: application/json header.

httpBodyBytes : String -> Bytes -> HttpBody

Put given Bytes in the body of your request. The string argument should be a MIME type to be used in the Content-Type header

import Bytes exposing (Bytes)
import Bytes.Encode
import Time
import Web
import Zip
import Zip.Entry


exampleZipBody : Web.HttpBody
exampleZipBody =
    Web.httpBodyBytes "application/zip"
        (Zip.fromEntries
            [ Bytes.Encode.string "Hello, World!"
                |> Bytes.Encode.encode
                |> Zip.Entry.store
                    { path = "hello.txt"
                    , lastModified = ( Time.utc, Time.millisToPosix 0 )
                    , comment = Nothing
                    }
            ]
            |> Zip.toBytes
        )

random

Helpers for randomness as part of an Interface. 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.randomUnsignedInt32s 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 }
                        )
}
randomUnsignedInt32s : 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

Helpers for console interactions as part of an Interface

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.

socket

Helpers for web sockets as part of an Interface

= SocketConnected SocketId
| SocketDisconnected
{ code : Int
, reason : String
}

An indication that connection has changed after having initiated Web.socketConnectTo.

  • SocketConnected: connection has been opened. Gives access to a Web.SocketId
  • SocketDisconnected: connection has been closed with
    • the close code sent by the server
    • The reason indicating why the server closed the connection, specific to the particular server and sub-protocol
= SocketId Int

Local identifier for a web socket for this session that can be used to communicate

connection

An Interface for opening a connection on a given address, notifying you when it's connected or disconnected

Once this detects it's available, make sure to set your state's SocketId so you can actually send and receive messages. And once it's disconnected, set your state's SocketId back to nothing:

case state.socketId of
    Nothing ->
        Web.socketConnectTo "ws://127.0.0.1:9000"
            |> Web.interfaceMap
                (\connectionChanged ->
                    case connectionChanged of
                        Web.SocketConnected socketId ->
                            { state | socketId = socketId |> Just }


                        Web.SocketDisconnected ->
                            { state | socketId = Nothing }
                )


    Just socketId ->
        Web.socketMessage socketId "Meow"
socketDisconnect : SocketId -> Interface future_

An Interface for closing a given connection

communicate

socketMessage : SocketId -> String -> Interface future_

An Interface for sending data to the server.

It's common to pair this with Json.Encode.encode 0 to send json.

socketMessageListen : SocketId -> Interface String

An Interface for detecting when data has been sent from the server

File download

Helpers for downloading a dynamically generated file as part of an Interface.

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 as part of an Interface.

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

Helpers for clipboard interactions as part of an Interface

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 as part of an Interface 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

window

Observe and alter the page's global environment as part of an Interface

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

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.

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 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
| 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)
| FileDownloadUnsignedInt8s
{ mimeType : String
, name : String
, content : List Int
}
| ClipboardReplaceBy String
| ClipboardRequest (String -> future)
| AudioSourceLoad
{ url : String
, on : Result AudioSourceLoadError AudioSource -> future
}
| AudioPlay Audio
| DomNodeRender
{ path : List Int
, node : DomTextOrElementHeader future
}
| NotificationAskForPermission ()
| NotificationShow
{ id : String
, message : String
, details : String
, on : NotificationClicked -> future
}
| HttpRequest (HttpRequest 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)
| SocketConnect
{ address : String
, on : SocketConnectionEvent -> future
}
| SocketMessage
{ id : SocketId
, data : String
}
| SocketDisconnect SocketId
| SocketMessageListen
{ id : SocketId
, on : String -> future
}
| 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 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.

type InternalFastDict key value
= InternalFastDict Int (InternalFastDictInner key value)

Alternative to Dict that internally allows fast creation from already sorted elements which significantly speeds up DOM interface creation.

type InternalFastDictInner key value
= InternalFastDictInnerNode
key
value
| InternalFastDictLeaf

Structure of an InternalFastDict

= InternalFastDictRed
| InternalFastDictBlack

The color of an InternalFastDictInner node. Leaves are considered Black.

{ old : InterfaceSingle future
, updated : InterfaceSingle future
}

What InterfaceSingleEdits are needed to sync up

= EditDom
{ path : List Int
, replacement : DomEdit
}
| EditAudio
{ url : String
, startTime : Posix
, replacement : AudioEdit
}
| EditNotification
{ id : String
, message : String
, details : String
}

Individual message to js to sync up with the latest interface type, describing changes to an existing interface with the same identity

= ReplacementAudioVolume AudioParameterTimeline
| ReplacementAudioSpeed AudioParameterTimeline
| ReplacementAudioStereoPan AudioParameterTimeline
| ReplacementAudioProcessing (List AudioProcessing)

What parts of an Audio are replaced

type DomEdit
= ReplacementDomNode (DomTextOrElementHeader ())
| ReplacementDomElementStyles
{ edit :
List
{ key : String
, value : String
}
, remove : List String
}
| ReplacementDomElementAttributes
{ edit :
List
{ key : String
, value : String
}
, remove : List String
}
| ReplacementDomElementAttributesNamespaced
{ edit :
List
{ namespace : String
, key : String
, value : String
}
, remove :
List
{ namespace : String
, key : String
}
}
| ReplacementDomElementStringProperties
{ edit :
List
{ key : String
, value : String
}
, remove : List String
}
| ReplacementDomElementBoolProperties
{ edit :
List
{ key : String
, value : Bool
}
, remove : List String
}
| ReplacementDomElementScrollToPosition
(Maybe
{ fromLeft : Float
, fromTop : Float
}
)
| ReplacementDomElementScrollToShow
(Maybe
}
)
| ReplacementDomElementScrollPositionRequest ()
| ReplacementDomElementEventListens
(List
{ key : String
}
)

What parts of a node are replaced. Either all modifiers of a certain kind or the whole node

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