A clean convention for sending messages via ports.
Thanks to @splodingsocks for the inspiration to not create a separate port for each Cmd/Sub. The Importance of Ports
Dropping into javascript to provide functionality not yet available in Elm
doesn't have to be the wild west. Instead of creating a port for every usecase,
why not create a single port for each direction required per module/feature then expose functions from your port module that simply build up a PortMessage
before its passed into your port.
We encode a string here in the payload but you can encode anything you need.
port module Document exposing (modifyTitle)
import Json.Encode
import PortMessage exposing (PortMessage)
modifyTitle : String -> Cmd a
modifyTitle title =
PortMessage.new "ModifyTitle"
|> PortMessage.withPayload (Json.Encode.string title)
|> document
port document : PortMessage -> Cmd a
None of the functions exposed from this API require any payload so the payload will simply be null
in js.
port module View exposing (enterFullscreen, leaveFullscreen, toggleFullscreen)
import PortMessage exposing (PortMessage)
enterFullscreen : Cmd a
enterFullscreen =
PortMessage.new "EnterFullscreen"
|> view
leaveFullscreen : Cmd a
leaveFullscreen =
PortMessage.new "LeaveFullscreen"
|> view
toggleFullscreen : Cmd a
toggleFullscreen =
PortMessage.new "ToggleFullscreen"
|> view
port view : PortMessage -> Cmd a
import Document
import View
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
TextInput value ->
({ model | text = value }, Document.modifyTitle value)
-- Note we don't have the awkward identity `()` argument from having
-- a port that doesn't require any additional data.
FullscreenBtnPressed ->
(model, View.enterFullscreen)
const app = Elm.App.fullscreen();
app.ports.document.subscribe({ tag, payload } => {
switch (tag) {
case 'ModifyTitle':
document.title = payload;
break;
}
});
app.ports.view.subscribe({ tag, payload } => {
// same as above but has cases for 'EnterFullscreen', 'LeaveFullscreen' and
// 'ToggleFullscreen'
});
How you structure your ports whether you decide to have a single Ports
module
or split it up into different modules by feature or API is entirely your choice
and this package will work either way. Personally I like having separate modules for each service File.elm
, Phoenix.elm
, Document.elm
over a single module for everything for several reasons.
-
Sharing code between projects. It's easy to just copy a small focused port module to another project without bringing along services that are not required.
-
The port modules can have other functionality that's not related to working with the port, for example the
File
module may exposeload : Filename -> Cmd a
whereFilename
is a type defined inFile.elm
, or maybebytesize : File -> Int
again whereFile
is a record alias defined inFile.elm
. -
It feels more natural, compare
Ports.join
,Ports.joinChannel
,Ports.joinPhoenixChannel
or my favouritePhoenix.join
-
Faster compile times.
I'm happy to receive any feedback and ideas for about additional features. Any input and pull requests are very welcome and encouraged. If you'd like to help or have ideas, get in touch with me at @Hendore in the elmlang Slack!