lue-bird/elm-state-interface/1.0.0

NOTE: the latest version is 2.0.4

Web

A state-interface program running in the browser

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.

  • state is what elm calls the model
  • An Interface can be created using the helpers in Web.Time, Web.Dom, Web.Http etc.

as elm/browser Program

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

Create an elm Program with a given Web.ProgramConfig. Short for

Platform.worker
    { init = \() -> Web.programInit yourAppConfig
    , update = Web.programUpdate yourAppConfig
    , subscriptions = Web.programSubscriptions yourAppConfig
    }
type ProgramState appState
= State
{ interface :
Emptiable
(KeysSet
(InterfaceSingle appState)
(InterfaceSingleKeys appState)
N1
)
Possibly
, appState : appState
}

Ignore the specific fields, this is just exposed so can annotate a program state like in

main : Program () (Web.State YourState) (Web.Event YourState)
main =
    Web.toProgram ...
type ProgramEvent appState
= InterfaceDiffFailedToDecode Error
| InterfaceEventDataFailedToDecode Error
| InterfaceEventIgnored
| AppEventToNewAppState appState

Ignore the specific variants, this is just exposed so can annotate a program event like in

main : Program () (Web.State YourState) (Web.Event YourState)
main =
    Web.toProgram ...

embed

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

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

interface types

type alias Interface state =
Rope (InterfaceSingle state)

Incoming and outgoing effects. To create one, use the helpers in Web.Time, .Dom, .Http etc.

To combine multiple, use Web.interfaceBatch and Web.interfaceNone

interfaceBatch : List (Interface state) -> Interface state

Combine multiple Interfaces into one.

interfaceNone : Interface state_

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 ]
    )
interfaceMap : (state -> mappedState) -> Interface state -> Interface mappedState

Map the state constructed by the Interface.

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

Web.Time.posixRequest
    |> Web.interfaceMap (\timeNow -> TimeReceived timeNow)

sometimes like elm's update

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


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


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

and sometimes like elm's Cmd.map/Task.map/Sub.map/...:

type State
    = MenuState Menu.State
    | PlayingState Playing.State


interface : State -> Interface State
interface state =
    case state of
        MenuState menuState ->
            Web.interfaceMap MenuState (Menu.interface menuState)


        PlayingState playingState ->
            Web.interfaceMap PlayingState (Playing.interface playingState)

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

DOM

Types used by Web.Dom

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

Plain text or a DomElement for use in an Interface.

type alias DomElement state =
RecordWithoutConstructorFunction
{ namespace : Maybe String
, tag : String
, styles : Dict String String
, attributes : Dict String String
, attributesNamespaced : Dict ( String, String ) String
, eventListens :
Dict
String
{ on : Value -> state
, defaultActionHandling : DefaultActionHandling
}
, subs : Array (DomNode state)
}

A tagged DOM node that can itself contain child nodes

= DefaultActionPrevent
| DefaultActionExecute

Setting for a listen Web.Dom.Modifier to keep or overwrite the browsers default action.

HTTP

Types used by Web.Http

type alias HttpRequest state =
RecordWithoutConstructorFunction
{ url : String
, method : String
, headers : List HttpHeader
, body : HttpBody
, expect : HttpExpect state
, timeout : Maybe Int
}

An HTTP request for use in an Interface.

You can set custom headers as needed. The timeout can be set to a number of milliseconds you are willing to wait before giving up

type alias HttpHeader =
( String, String )

An Http header for configuring a request.

= HttpBodyEmpty
| HttpBodyString
{ mimeType : String
, content : String
}

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.

type HttpExpect state
= HttpExpectJson (Result HttpError Value -> state)
| HttpExpectString (Result HttpError String -> state)
| HttpExpectWhatever (Result HttpError () -> state)

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

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

A Request can fail in a couple ways:

  • BadUrl means you did not provide a valid URL.
  • Timeout means it took too long to get a response.
  • NetworkError means the user turned off their wifi, went in a cave, etc.
  • 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.
  • BadBody means you got a response back with a nice status code, but the body of the response was something unexpected. Contains:
    • The response Metadata.
    • The raw response body as a Json.Decode.Value.
    • The Json.Decode.Error that caused the error.
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.

internals, safe to ignore

type InterfaceSingle state
= InterfaceWithReceive (InterfaceSingleWithReceive state)
| InterfaceWithoutReceive InterfaceSingleWithoutReceive

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

= TimePosixRequest (Posix -> state)
| TimezoneOffsetRequest (Int -> state)
| TimezoneNameRequest (ZoneName -> state)
| TimePeriodicallyListen
{ intervalDurationMilliSeconds : Int
, on : Posix -> state
}
| RandomUnsignedInt32sRequest
{ count : Int
, on : List Int -> state
}
| DocumentEventListen
{ eventName : String
, on : Decoder state
}
| DomNodeRender (DomNode state)
| HttpRequest (HttpRequest state)
| WindowSizeRequest
( { width : Int
, height : Int
}
-> state
)
| WindowEventListen
{ eventName : String
, on : Decoder state
}
| WindowAnimationFrameListen (Posix -> state)
| NavigationUrlRequest (AppUrl -> state)
| ClipboardRequest (String -> state)

An InterfaceSingle that will notify elm some time in the future.

= ConsoleLog String
| ConsoleWarn String
| ConsoleError String
| NavigationReplaceUrl AppUrl
| NavigationPushUrl AppUrl
| NavigationGo Int
| NavigationLoad Url
| NavigationReload
| FileDownloadUnsignedInt8s
{ mimeType : String
, name : String
, content : List Int
}
| ClipboardReplaceBy String

An InterfaceSingle that will never notify elm

= InterfaceWithReceiveDiff InterfaceWithReceiveDiff
| InterfaceWithoutReceiveDiff InterfaceWithoutReceiveDiff

Safe to ignore. Individual messages to js. Also used to identify responses with the same part in the interface

= AddTimePosixRequest
| AddTimezoneOffsetRequest
| AddTimezoneNameRequest
| AddTimePeriodicallyListen { milliSeconds : Int }
| AddRandomUnsignedInt32sRequest Int
| AddDocumentEventListen String
| AddEditDom EditDomDiff
| AddHttpRequest HttpRequestId
| AddWindowSizeRequest
| AddWindowEventListen String
| AddWindowAnimationFrameListen
| AddNavigationUrlRequest
| AddClipboardRequest

Actions that will notify elm some time in the future

= AddConsoleLog String
| AddConsoleWarn String
| AddConsoleError String
| AddNavigationReplaceUrl AppUrl
| AddNavigationPushUrl AppUrl
| AddNavigationGo Int
| AddNavigationLoad Url
| AddNavigationReload
| AddFileDownloadUnsignedInt8s
{ mimeType : String
, name : String
, content : List Int
}
| AddClipboardReplaceBy String
| RemoveTimePeriodicallyListen { milliSeconds : Int }
| RemoveHttpRequest HttpRequestId
| RemoveDom
| RemoveWindowEventListen String
| RemoveWindowAnimationFrameListen
| RemoveDocumentEventListen String

Actions that will never notify elm again

type alias EditDomDiff =
RecordWithoutConstructorFunction
{ path : List Int
, replacement : ReplacementInEditDomDiff
}

Change the current node at a given path using a given ReplacementInEditDomDiff

= ReplacementDomNode DomNodeId
| ReplacementDomElementStyles (Dict String String)
| ReplacementDomElementAttributes (Dict String String)
| ReplacementDomElementAttributesNamespaced
(Dict ( String, String ) String)
| ReplacementDomElementEventListens
(Dict String DefaultActionHandling)

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

Safe to ignore. Identification and order of an Interface

Safe to ignore. Tag for the ordering of an InterfaceSingleId

= IdInterfaceWithReceive InterfaceSingleWithReceiveId
| IdInterfaceWithoutReceive InterfaceSingleWithoutReceive

Safe to ignore. Identifier for an Interface

= IdTimePosixRequest
| IdTimezoneOffsetRequest
| IdTimezoneNameRequest
| IdTimePeriodicallyListen { milliSeconds : Int }
| IdRandomUnsignedInt32sRequest Int
| IdDomNodeRender
| IdHttpRequest HttpRequestId
| IdWindowSizeRequest
| IdWindowEventListen String
| IdWindowAnimationFrameListen
| IdNavigationUrlRequest
| IdDocumentEventListen String
| IdClipboardRequest

Safe to ignore. Identifier for an InterfaceSingleWithReceive

Safe to ignore. Tag for the identification mapping of an InterfaceSingle → InterfaceSingleId

type alias DomElementId =
RecordWithoutConstructorFunction
{ namespace : Maybe String
, tag : String
, styles : Dict String String
, attributes : Dict String String
, attributesNamespaced : Dict ( String, String ) String
, eventListens : Dict String DefaultActionHandling
, subs : Array DomNodeId
}

Safe to ignore. Identifier for a DomElement

= DomTextId String
| DomElementId DomElementId

Safe to ignore. Identifier for a DomNode

type alias HttpRequestId =
RecordWithoutConstructorFunction
{ url : String
, method : String
, headers : List HttpHeader
, body : HttpBody
, expect : HttpExpectId
, timeout : Maybe Int
}

Safe to ignore. Identifier for an HttpRequest

= IdHttpExpectJson
| IdHttpExpectString
| IdHttpExpectWhatever

Safe to ignore. Identifier for an HttpExpect