I’ve been implementing RealWorld in Elm from scratch and one architectural design decision I made early on was adopting my own version of the translator pattern for child-parent communication.
My version of the translator pattern leads to an update
function, for the login page, that looks as follows:
type alias UpdateOptions msg =
{ ...
, onLoggedIn : User -> msg
, onChange : Msg -> msg
}
type Msg
= ...
| SubmittedForm
| GotLoginResponse (Result (Api.Error (List String)) User
update : UpdateOptions msg -> Msg -> Model -> ( Model, Cmd msg )
update options msg model =
case msg of
...
SubmittedForm ->
validate model
|> V.withValidation
{ onSuccess =
\{ email, password } ->
( { model | errorMessages = [], isDisabled = True }
--
-- 1. Attempt to login.
--
, Login.login
options.apiUrl
{ email = email
, password = password
, onResponse = GotLoginResponse
}
|> Cmd.map options.onChange
)
, onFailure =
\errorMessages ->
( { model | errorMessages = errorMessages }
, Cmd.none
)
}
GotLoginResponse result ->
Api.handleFormResponse
(\user ->
( init
--
-- 2. On successfully logging in you have to tell your parent, Main in this case,
-- so that they can do whatever they need to do on log in. As the login page
-- I don't care about that stuff. That's for my parent to handle.
-- In the case of Main, it wants to record the user's token and redirect the
-- user to the home page.
--
, Task.dispatch (options.onLoggedIn user)
)
)
model
result
I like the solution that unfolds and I think I’m going to continue using it but there is at least one bit that maybe contentious which is the use of Task.dispatch
.
dispatch : msg -> Cmd msg
dispatch =
Task.succeed >> Task.perform identity
With Task.dispatch
I avoid making update have the return type ( Model, msg, Cmd msg )
and instead I push the parent message into the Elm runtime which eventually finds its way back to the parent’s update function.
Here’s the relevant parts of Main
:
updateLoginPage : LoginPage.Msg -> SuccessModel -> ( SuccessModel, Cmd Msg )
updateLoginPage pageMsg subModel =
case subModel.page of
Login pageModel ->
let
( newPageModel, newPageCmd ) =
LoginPage.update
{ apiUrl = subModel.apiUrl
--
-- 1. This is saying to notify me when you've successfully logged in via the login page.
--
, onLoggedIn = LoggedIn
, onChange = ChangedPage << ChangedLoginPage
}
pageMsg
pageModel
in
( { subModel | page = Login newPageModel }
, newPageCmd
)
_ ->
( subModel, Cmd.none )
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
...
LoggedIn user ->
--
-- 2. Handle a successful login.
--
loginUser user model
...
loginUser : User -> Model -> ( Model, Cmd Msg )
loginUser user =
withSuccessModel
(\subModel ->
( { subModel | viewer = Viewer.User user }
, Cmd.batch
--
-- 3. Save the user's token and redirect to the home page.
--
[ Port.Action.saveToken user.token
, Route.redirectToHome subModel.key
]
)
)
N.B. If I used ( Model, msg, Cmd msg )
then loginUser
would be handled in the updateLoginPage
function.
I’d love to get feedback on this design so I can be more objective about this architectural decision, i.e. this take on the translator pattern.
P.S. I make use of this version of the translator pattern in dwayne/elm-debouncer. I liked it so much for that library that I decided to try it out in this app as well. At work we adopted a version of the OutMsg
pattern and I didn’t much like the code it lead you to write.