lue-bird / elm-state-interface / Web

A state-interface program that can run in the browser

program : { initialState : state, interface : state -> Interface state, ports : { toJs : Json.Encode.Value -> Platform.Cmd.Cmd Basics.Never, fromJs : (Json.Encode.Value -> ProgramEvent state) -> Platform.Sub.Sub (ProgramEvent state) } } -> Program state

Create a Program:

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 ]
    )

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 Basics.Bool
, scrollToPosition : Maybe { fromLeft : Basics.Float
, fromTop : Basics.Float }
, scrollToShow : Maybe { x : DomElementVisibilityAlignment
, y : DomElementVisibilityAlignment }
, scrollPositionRequest : Maybe ({ fromLeft : Basics.Float
, fromTop : Basics.Float } -> future)
, eventListens : Dict String { on : Json.Decode.Value -> future
, defaultActionHandling : DefaultActionHandling } 
}

Everything about a tagged DOM element except potential sub-nodes


type DomElementVisibilityAlignment
    = DomElementStart
    | DomElementEnd
    | DomElementCenter

What part of the Web.Dom.Element should be visible


type DefaultActionHandling
    = 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 : Time.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
                    )
            )
        )


type AudioSourceLoadError
    = AudioSourceLoadDecodeError
    | AudioSourceLoadNetworkError

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


type AudioProcessing
    = AudioLinearConvolution ({ sourceUrl : String })
    | AudioLowpass ({ cutoffFrequency : AudioParameterTimeline })
    | AudioHighpass ({ cutoffFrequency : AudioParameterTimeline })

A single effect filter applied to an Audio


type alias AudioParameterTimeline =
{ startValue : Basics.Float
, keyFrames : List { time : Time.Posix
, value : Basics.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.


type HttpBody
    = HttpBodyEmpty
    | HttpBodyString ({ mimeType : String, content : String })
    | HttpBodyUnsignedInt8s ({ mimeType : String, content : List Basics.Int })

Data send in your http request.


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.


type HttpError
    = HttpBadUrl
    | HttpNetworkError
    | HttpBadStatus ({ metadata : HttpMetadata, body : Json.Decode.Value })

A Request can fail in a couple ways:


type alias HttpMetadata =
RecordWithoutConstructorFunction { url : String
, statusCode : Basics.Int
, statusText : String
, headers : Dict String String 
}

Extra information about the response:

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


type SocketConnectionEvent
    = SocketConnected SocketId
    | SocketDisconnected ({ code : Basics.Int, reason : String })

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


type SocketId
    = SocketId Basics.Int

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

geo location

Types used by Web.GeoLocation


type alias GeoLocation =
RecordWithoutConstructorFunction { latitudeInDecimalDegrees : Basics.Float
, longitudeInDecimalDegrees : Basics.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 : Basics.Float
, y : Basics.Float }
, thumbstickRight : { x : Basics.Float
, y : Basics.Float }
, thumbsticksAdditional : List { x : Basics.Float
, y : Basics.Float } 
}

Controller information on button presses, thumbstick positions etc.

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


type GamepadButton
    = GamepadButtonPressed ({ firmnessPercentage : Basics.Float })
    | GamepadButtonReleased ({ isTouched : Basics.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


type NotificationClicked
    = NotificationClicked

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

window

Types used by Web.Window


type WindowVisibility
    = WindowShown
    | WindowHidden

The visibility to the user

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 : Json.Encode.Value -> Platform.Cmd.Cmd Basics.Never
, fromJs : (Json.Encode.Value -> ProgramEvent state) -> Platform.Sub.Sub (ProgramEvent state) } 
}

What's needed to create a state-interface program

programInit : ProgramConfig state -> ( ProgramState state, Platform.Cmd.Cmd (ProgramEvent state) )

The "init" part for an embedded program

programUpdate : ProgramConfig state -> ProgramEvent state -> ProgramState state -> ( ProgramState state, Platform.Cmd.Cmd (ProgramEvent state) )

The "update" part for an embedded program

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 : FastDict.Dict String (InterfaceSingle appState), appState : appState })

The "model" in a Web.program


type ProgramEvent appState
    = JsEventFailedToDecode Json.Decode.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 : Json.Decode.Decoder future })
    | ConsoleLog String
    | ConsoleWarn String
    | ConsoleError String
    | NavigationReplaceUrl AppUrl
    | NavigationPushUrl AppUrl
    | NavigationGo Basics.Int
    | NavigationLoad String
    | NavigationReload
    | NavigationUrlRequest (AppUrl -> future)
    | FileDownloadUnsignedInt8s ({ mimeType : String, name : String, content : List Basics.Int })
    | ClipboardReplaceBy String
    | ClipboardRequest (String -> future)
    | AudioSourceLoad ({ url : String, on : Result AudioSourceLoadError AudioSource -> future })
    | AudioPlay Audio
    | DomNodeRender ({ path : List Basics.Int, node : DomTextOrElementHeader future })
    | NotificationAskForPermission
    | NotificationShow ({ id : String, message : String, details : String, on : NotificationClicked -> future })
    | HttpRequest (HttpRequest future)
    | TimePosixRequest (Time.Posix -> future)
    | TimezoneOffsetRequest (Basics.Int -> future)
    | TimeOnce ({ pointInTime : Time.Posix, on : Time.Posix -> future })
    | TimePeriodicallyListen ({ intervalDurationMilliSeconds : Basics.Int, on : Time.Posix -> future })
    | TimezoneNameRequest (String -> future)
    | RandomUnsignedInt32sRequest ({ count : Basics.Int, on : List Basics.Int -> future })
    | WindowSizeRequest ({ width : Basics.Int, height : Basics.Int } -> future)
    | WindowPreferredLanguagesRequest (List String -> future)
    | WindowEventListen ({ eventName : String, on : Json.Decode.Decoder future })
    | WindowVisibilityChangeListen (WindowVisibility -> future)
    | WindowAnimationFrameListen (Time.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 Basics.Int Gamepad -> future)
    | GamepadsChangeListen (Dict Basics.Int Gamepad -> future)

A "non-batched" Interface. To create one, use the helpers in Web.Time, Web.Dom, Web.Http etc.

interfaceSingleEdits : { old : InterfaceSingle future, updated : InterfaceSingle future } -> List InterfaceSingleEdit

What InterfaceSingleEdits are needed to sync up


type InterfaceSingleEdit
    = EditDom ({ path : List Basics.Int, replacement : DomEdit })
    | EditAudio ({ url : String, startTime : Time.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


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

What parts of an Audio are replaced