A state-interface program that can run in the browser
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.
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
Combine multiple Interfaces into one
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 ]
)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 TreeUiEventIn all these examples, you end up converting the narrow future representation of part of the interface to a broader representation for the parent interface
An Interface for getting the current POSIX time.
Replacement for elm/time's Time.now.
An Interface for getting a Time.Zone
based on the current UTC offset.
Replacement for elm/time's Time.here.
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.
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.
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.
These are primitives used for SVG and HTML
(filling the same role as elm/virtual-dom)
Everything about a tagged DOM element except potential sub-nodes
What part of the Web.domElement should be visible
DomElementStart: mostly for text to readDomElementEnd: mostly for text to writeDomElementCenter: mostly for imagesSetting for a listen Web.DomModifier
to keep or overwrite the browser's default action
Plain text or an DomElement. Create using domText and domElement
A tagged DOM node that can itself contain child nodes
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
Create an SVG element Web.DomNode.
with a given tag, DomModifiers and sub-nodes
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 subsWire 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
= ClickedAn Interface for displaying a given Web.DomNode
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.
class in <div class="greeting"></div>className in div.className = "greeting"But don't be surprised: There are cases where
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" FalseMaybe 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.
Wire events from this DomModifier to a specific event.
Web.domListenTo "click" |> Web.domModifierFutureMap (\_ -> ButtonClicked)Combine multiple DomModifiers into one.
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 ]
)A key-value attribute DomModifier
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" valueA key-value style DomModifier
A key-bool value DOM property DomModifier
A key-string value DOM property DomModifier
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
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
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.
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
An Interface for getting the current page's app-specific URL.
Is usually used while starting up the app.
Note: Uses window.location.href
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
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
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
Helpers for HTTP requests as part of an Interface
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.
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
Describe what you expect to be returned in an http response body.
A Request can fail in a couple ways:
BadUrl means you did not provide a valid URLNetworkError 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 errorBadStatus means you got a response back, but the status code indicates failure. Contains:Metadata.Json.Decode.Value.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. The values are separated by commas, following the rules outlined here.
An Interface for handling an HttpRequest
using the fetch API
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.
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.
Add custom headers to the Web.HttpRequest.
request
|> Web.httpAddHeaders
[ ( "X-Custom-Header", "ProcessThisImmediately" )
]Expect the response body to be a String.
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 succeedOk if there was a result with eitherOk with the decoded valueErr with a Json.Decode.Error
and the actual text responseDiscard the response body.
Put a given JSON value in the body of your request. This will automatically add the Content-Type: application/json header.
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
)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 }
)
}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
Helpers for console interactions as part of an Interface
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
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
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
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.
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
)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
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
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.
An Interface for keeping an eye on
when the local storage on a different tab with the same url origin is removed.
Helpers for web sockets as part of an Interface
An indication that connection has changed
after having initiated Web.socketConnectTo.
SocketConnected: connection has been opened. Gives access to a Web.SocketIdSocketDisconnected: connection has been closed withcode sent by the serverreason indicating why the server closed the connection, specific to the particular server and sub-protocolLocal identifier for a web socket for this session that can be used to communicate
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"An Interface for closing a given connection
An Interface for sending data to the server.
It's common to pair this with Json.Encode.encode 0
to send json.
An Interface for detecting when data has been sent from the server
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.
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
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"
}Some kind of sound we want to play. To create Audio, start with Web.audioFromSource
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
)
)
)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.A single effect filter applied to an Audio
defining how loud a sound should be at any point in time
An Interface for fetching audio data from a given url
and returning an AudioSource to use with audioFromSource.
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.
An Interface for playing Audio created with audioFromSource.
To play multiple audios:
[ audio0, audio1, audio2 ]
|> List.map Web.audioPlay
|> Web.interfaceBatchScale 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.
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!
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
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.
Frequencies below a given cutoff parameter are attenuated; frequencies above it pass through.
Has a 12dB/octave rolloff and no peak at the cutoff.
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:
Set it to a constant value. Add audioParameterThrough to make it transition from this starting value over time
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 0Duration is from ianmackenzie/elm-unitsTime is from elm/timeYou do not have to worry about order.
Helpers for clipboard interactions as part of an Interface
An Interface for reading the textual contents of the system clipboard.
Note: uses navigator.clipboard.readText
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
Observe the GeoLocation as part of an Interface
using the web geolocation API.
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.
An Interface for getting the current position of the device
An Interface for detecting changes in the current position of the device
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.interfaceBatchIf 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)
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
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.
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
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.
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.
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.interfaceNoneNote: If you haven't previously used notificationAskForPermission
it will request this permission now.
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.interfaceBatchObserve and alter the page's global environment as part of an Interface
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
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 appWindowShown otherwiseAn Interface for getting the inner window width and height in pixels,
not including toolbars/scrollbars
An Interface for detecting changes to the inner window width and height
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
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
An Interface for detecting a specific document event
that has no native Interface, like like scroll, scrollend, selectionchange or paste
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:
An Interface for setting the document's title
An Interface for adding or replacing the document's author metadata
An Interface for adding or replacing the document's keywords metadata
which should consist of words relevant to the page's content
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.
If you just want to replace a part of your elm app with this architecture. Make sure to wire in all 3:
What's needed to create a state-interface program
The "init" part for an embedded program
The "update" part for an embedded program
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
}Exposed so can for example simulate it more easily in tests, add a debugger etc.
An individual DomModifier.
Create using domAttribute, domStyle, domListenTo etc.
The "model" in a Web.program
The "msg" in a Web.program
Plain text or a DomElementHeader for use in an Interface.
Alternative to Dict optimized for fast merge and fast creation.
Would be a terrible fit if we needed fast insert and get.
What InterfaceSingleEdits are needed to sync up
Individual message to js to sync up with the latest interface type, describing changes to an existing interface with the same identity
What parts of an Audio are replaced
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
Create a
Program:The state is everything the program knows (what The Elm Architecture calls model). And it always starts with a given
initialStateThe
Interfaceis the face to the outside world and can be created using the helpers in time, DOM, HTTP etc. The giveninterfacefunction constructs these interfaces based on the current stateConnections to and from js