lue-bird/elm-state-interface/2.0.0

NOTE: the latest version is 2.0.4

Web

A state-interface program that can run in the browser

{ initialState : state
, interface : state -> Interface state
, ports :
{ toJs : Value -> Cmd Never
, 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 Web.Dom, Web.Time, Web.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 =
Rope (InterfaceSingle future)

Incoming and outgoing effects. To create one, use the helpers in Web.Time, Web.Dom, Web.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.Time.posixRequest
    |> Web.interfaceFutureMap (\timeNow -> TimeReceived timeNow)


button "show all entries"
    |> Web.Dom.render
    |> 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.Dom.render


treeUi : ... -> Web.Dom.Node 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

That's it. Everything else below is types used in the other modules like Web.Time, Web.Dom, Web.Http etc. Leave them be and look at those modules :)

DOM

Types used by Web.Dom

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

Everything about a tagged DOM element except potential sub-nodes

= DomElementStart
| DomElementEnd
| DomElementCenter

What part of the Web.Dom.Element 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.Dom.Modifier to keep or overwrite the browser's default action

Audio

Types used by Web.Audio

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.Audio.fromSource

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

Audio data we can use to play sounds. Use Web.Audio.sourceLoad 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.Audio.fromSource 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

HTTP

Types used by Web.Http

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.Http.addHeaders to set custom headers as needed. Use Web.Time.onceAt 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.Http.jsonBody looks like this:

    import Json.Encode
    
    
    jsonBody : Json.Encode.Value -> Web.HttpBody
    jsonBody 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.Http.bodyBytes

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 : Dict 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 dictionary. The values are separated by commas, following the rules outlined here.

socket

Types used by Web.Socket

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

An indication that connection has changed after having initiated Web.Socket.connectTo.

  • 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

Identifier for a Web.Socket that can be used to communicate

geo location

Types used by Web.GeoLocation

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

gamepads

Types used by Web.Gamepads

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.

notification

Types used by Web.Notification

= NotificationClicked

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

window

Types used by Web.Window

= 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

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 Never
, 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 ProgramState appState
= State
{ interface : Dict String (InterfaceSingle 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 Web.Time, Web.Dom, Web.Http etc.

= DomText String
| DomElementHeader (DomElementHeader future)

Plain text or a DomElementHeader for use in an Interface.

To simulate diffing of interfaces, use

FastDict.merge
    (\id toAdd -> ..add toAdd..)
    (\id old updated -> ..edit (interfaceSingleEdits { old, updated })..)
    (id toRemove -> ..remove toRemove..)
    updatedInterfaces
    oldInterfaces
    ..what you fold into..

see FastDict.merge

{ 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
(Dict String DefaultActionHandling)

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