Skip to content

Commit

Permalink
Web helpers prefix
Browse files Browse the repository at this point in the history
  • Loading branch information
lue-bird committed Nov 30, 2023
1 parent 4b4eb36 commit 4560c3a
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 65 deletions.
46 changes: 24 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,56 +10,58 @@ which means
- update is part of the interface (having an intermediate event type is optional but often useful)
- when updating based on an event, there's no need to case on the relevant state. (Either use the state from the `case` in the interface in an inner update, or safely include the state in the event)

The classic counter example including changing url:
The classic counter example including managing the url:

```elm
import Web
import Web.Dom
import AppUrl -- lydell/elm-app-url
import AppUrl exposing (AppUrl) -- lydell/elm-app-url
import Json.Encode -- elm/json

type alias State =
Int

type Event
= DecreaseClicked
| IncreaseClicked
= MinusClicked
| PlusClicked
| UserWentToUrl AppUrl

app : Web.Config State
app =
programConfig : Web.ProgramConfig State
programConfig =
{ initialState = 0
, interface =
\counter ->
[ Web.Dom.element "div"
[]
[ Web.Dom.element "button"
[ Web.Dom.listenTo "click" |> Web.Dom.modifierMap (\_ -> IncreaseClicked) ]
[ Web.Dom.listenTo "click" ]
[ "+" |> Web.Dom.text ]
|> Web.Dom.map (\_ -> PlusClicked)
, Web.Dom.element "div"
[]
[ counter |> String.fromInt |> Web.Dom.text ]
, Web.Dom.element "button"
[ Web.Dom.listenTo "click" |> Web.Dom.modifierMap (\_ -> DecreaseClicked) ]
[ Web.Dom.listenTo "click" ]
[ "-" |> Web.Dom.text ]
|> Web.Dom.map (\_ -> MinusClicked)
]
|> Web.Dom.render
, Web.Navigation.pushUrl
{ path = []
, queryParameters = Dict.singleton "counter" [ counter |> String.fromInt ]
, fragment = Nothing
}
, Web.Navigation.urlRequest |> Web.map UserWentToUrl
, Web.Navigation.byUserListen |> Web.map UserWentToUrl
, Web.Navigation.urlRequest |> Web.interfaceMap UserWentToUrl
, Web.Navigation.byUserListen |> Web.interfaceMap UserWentToUrl
]
|> Web.batch
|> Web.map
|> Web.interfaceBatch
|> Web.interfaceMap
(\event ->
case event of
DecreaseClicked ->
MinusClicked ->
counter - 1

IncreaseClicked ->
PlusClicked ->
counter + 1

UserWentToUrl newUrl ->
Expand All @@ -72,9 +74,9 @@ app =
, ports = { fromJs = fromJs, toJs = toJs }
}

main : Program () (Web.State State) (Web.Event State)
main : Program () (Web.ProgramState State) (Web.ProgramEvent State)
main =
app |> Web.toProgram
Web.program programConfig

port toJs : Json.Encode.Value -> Cmd event_
port fromJs : (Json.Encode.Value -> event) -> Sub event
Expand All @@ -89,7 +91,7 @@ in js
import * as Web from "@lue-bird/elm-state-interface"

const elmApp = Elm.Main.init({});
Web.start({
Web.programStart({
elmPorts : elmApp.ports,
domElement : document.getElementById("your-app-element")
})
Expand Down Expand Up @@ -160,19 +162,19 @@ type alias State =
_ ->
[ case state.icon of
Ok _ ->
Web.none
Web.interfaceNone
Err _ ->
Http.request { url = "...", decoder = Image.jsonDecoder }
|> Web.map (\result -> { state | icon = result })
|> Web.interfaceMap (\result -> { state | icon = result })
, case state.content of
Ok _ ->
Web.none
Web.interfaceNone
Err _ ->
Http.request { url = "...", decoder = Json.Decode.string }
|> Web.map (\result -> { state | content = result })
|> Web.interfaceMap (\result -> { state | content = result })
, ..error ui..
]
|> Web.batch
|> Web.interfaceBatch
}
```
which feels a bit more explicit, declarative and less wiring-heavy at least.
Expand Down
2 changes: 1 addition & 1 deletion runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface ElmPorts {
fromJs: { send: (toElm: any) => void };
}

export function start(appConfig: { ports: ElmPorts, domElement: HTMLElement }) {
export function programStart(appConfig: { ports: ElmPorts, domElement: HTMLElement }) {
const interfaceImplementations: { [key: string]: (config: any, sendToElm: (v: any) => void) => void } = {
"addTimePosixRequest": (_config, sendToElm) => {
sendToElm(Date.now())
Expand Down
82 changes: 41 additions & 41 deletions src/Web.elm
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module Web exposing
( Config
, toProgram, State(..), Event(..)
, init, subscriptions, update
, Interface, InterfaceSingle(..), batch, none, map
( ProgramConfig
, program, ProgramState(..), ProgramEvent(..)
, programInit, programUpdate, programSubscriptions
, Interface, InterfaceSingle(..), interfaceBatch, interfaceNone, interfaceMap
, DomNode(..), DomElement
, HttpRequest, HttpHeader, HttpBody(..), HttpExpect(..), HttpError(..), HttpMetadata
, InterfaceDiff(..)
Expand All @@ -12,24 +12,24 @@ module Web exposing

{-| A state-interface program running in the browser
@docs Config
@docs ProgramConfig
## Program
## as elm Program
@docs toProgram, State, Event
@docs program, ProgramState, ProgramEvent
## embed
If you just want to replace a part of your elm app with this architecture. Make sure to wire in all 3:
@docs init, subscriptions, update
@docs programInit, programUpdate, programSubscriptions
# interface types
@docs Interface, InterfaceSingle, batch, none, map
@docs Interface, InterfaceSingle, interfaceBatch, interfaceNone, interfaceMap
## DOM
Expand Down Expand Up @@ -83,7 +83,7 @@ import Url exposing (Url)
Web.toProgram ...
-}
type State appState
type ProgramState appState
= State
{ interface : Emptiable (KeysSet (InterfaceSingle appState) (InterfaceSingleKeys appState) N1) Possibly
, appState : appState
Expand Down Expand Up @@ -114,21 +114,21 @@ type InterfaceSingleToIdTag
- An [`Interface`](#Interface) can be created using the helpers in `Web.Time`, `Web.Dom`, `Web.Http` etc.
-}
type alias Config state =
type alias ProgramConfig state =
RecordWithoutConstructorFunction
{ initialState : state
, interface : state -> Interface state
, ports :
{ toJs : Json.Encode.Value -> Cmd Never
, fromJs : (Json.Encode.Value -> Event state) -> Sub (Event state)
, fromJs : (Json.Encode.Value -> ProgramEvent state) -> Sub (ProgramEvent state)
}
}


{-| Incoming and outgoing effects.
To create one, use the helpers in `Web.Time`, `.Dom`, `.Http` etc.
To combine multiple, use [`Web.batch`](#batch) and [`Web.none`](#none)
To combine multiple, use [`Web.interfaceBatch`](#interfaceBatch) and [`Web.interfaceNone`](#interfaceNone)
-}
type alias Interface state =
Expand Down Expand Up @@ -299,25 +299,25 @@ type DomNodeId

{-| Combine multiple [`Interface`](#Interface)s into one.
-}
batch : List (Interface state) -> Interface state
batch =
interfaceBatch : List (Interface state) -> Interface state
interfaceBatch =
\interfaces -> interfaces |> Rope.fromList |> Rope.concat


{-| Doing nothing as an [`Interface`](#Interface). These two examples are equivalent:
Web.batch [ a, Web.none, b ]
Web.interfaceBatch [ a, Web.interfaceNone, b ]
and
Web.batch
Web.interfaceBatch
(List.filterMap identity
[ a |> Just, Nothing, b |> Just ]
)
-}
none : Interface state_
none =
interfaceNone : Interface state_
interfaceNone =
Rope.empty


Expand All @@ -326,12 +326,12 @@ none =
In practice, this is sometimes used like a kind of event-config pattern:
Web.Time.posixRequest
|> Web.map (\timeNow -> TimeReceived timeNow)
|> Web.interfaceMap (\timeNow -> TimeReceived timeNow)
sometimes like elm's `update`
...
|> Web.map
|> Web.interfaceMap
(\event ->
case event of
MouseMovedTo newMousePoint ->
Expand All @@ -354,17 +354,17 @@ and sometimes like elm's `Cmd.map/Task.map/Sub.map/...`:
interface state =
case state of
MenuState menuState ->
Web.map MenuState (Menu.interface menuState)
Web.interfaceMap MenuState (Menu.interface menuState)
PlayingState playingState ->
Web.map PlayingState (Playing.interface 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
-}
map : (state -> mappedState) -> (Interface state -> Interface mappedState)
map stateChange =
interfaceMap : (state -> mappedState) -> (Interface state -> Interface mappedState)
interfaceMap stateChange =
\interface ->
interface
|> Rope.toList
Expand Down Expand Up @@ -503,7 +503,7 @@ domElementMap stateChange =
Web.toProgram ...
-}
type Event appState
type ProgramEvent appState
= InterfaceDiffFailedToDecode Json.Decode.Error
| InterfaceEventDataFailedToDecode Json.Decode.Error
| InterfaceEventIgnored
Expand Down Expand Up @@ -1224,8 +1224,8 @@ httpBodyToJson =

{-| The "init" part for an embedded program
-}
init : Config state -> ( State state, Cmd (Event state) )
init appConfig =
programInit : ProgramConfig state -> ( ProgramState state, Cmd (ProgramEvent state) )
programInit appConfig =
let
initialInterface : Emptiable (KeysSet (InterfaceSingle state) (InterfaceSingleKeys state) N1) Possibly
initialInterface =
Expand Down Expand Up @@ -1263,8 +1263,8 @@ listFirstJust tryMapToFound list =

{-| The "subscriptions" part for an embedded program
-}
subscriptions : Config state -> (State state -> Sub (Event state))
subscriptions appConfig =
programSubscriptions : ProgramConfig state -> (ProgramState state -> Sub (ProgramEvent state))
programSubscriptions appConfig =
\(State state) ->
-- re-associate event based on current interface
appConfig.ports.fromJs
Expand Down Expand Up @@ -1805,8 +1805,8 @@ domElementIdJsonDecoder =

{-| The "update" part for an embedded program
-}
update : Config state -> (Event state -> State state -> ( State state, Cmd (Event state) ))
update appConfig =
programUpdate : ProgramConfig state -> (ProgramEvent state -> ProgramState state -> ( ProgramState state, Cmd (ProgramEvent state) ))
programUpdate appConfig =
\event ->
case event of
InterfaceEventIgnored ->
Expand Down Expand Up @@ -1975,19 +1975,19 @@ type InterfaceDiff


{-| Create an elm [`Program`](https://dark.elm.dmy.fr/packages/elm/core/latest/Platform#Program)
with a given [`Web.Config`](#Config). Short for
with a given [`Web.ProgramConfig`](#ProgramConfig). Short for
Platform.worker
{ init = \() -> Web.init yourAppConfig
, update = Web.update yourAppConfig
, subscriptions = Web.subscriptions yourAppConfig
{ init = \() -> Web.programInit yourAppConfig
, update = Web.programUpdate yourAppConfig
, subscriptions = Web.programSubscriptions yourAppConfig
}
-}
toProgram : Config state -> Program () (State state) (Event state)
toProgram appConfig =
program : ProgramConfig state -> Program () (ProgramState state) (ProgramEvent state)
program appConfig =
Platform.worker
{ init = \() -> init appConfig
, update = update appConfig
, subscriptions = subscriptions appConfig
{ init = \() -> programInit appConfig
, update = programUpdate appConfig
, subscriptions = programSubscriptions appConfig
}
2 changes: 1 addition & 1 deletion src/Web/Window.elm
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ sizeRequest =
resizeListen : Web.Interface (Result Json.Decode.Error { width : Int, height : Int })
resizeListen =
eventListen "resize"
|> Web.map
|> Web.interfaceMap
(\value ->
value
|> Json.Decode.decodeValue
Expand Down

0 comments on commit 4560c3a

Please sign in to comment.