I wanted to replay the user interaction with an Elm app, including the scrolling of the pages.
Something similar to what David Gilbertson nicely describe in his article A user encounters a JavaScript error. You’ll never guess what happens next!!
Elm has an official “History Explorer” that already does a good job out of the box, but it doesn’t replay commands, that are needed to change the scrolling position of the page.
The reason why it doesn’t replay commands is because these commands are causing side effect and it would not be safe to have your app effecting the environment (for example changing a database) while replaying an interaction.
Let’s see more in details what would be the implication or replaying commands.
The signature of the update
function in Elm is, in case you use Html.beginnerProgram:
update : Msg -> Model -> Model
for Html.program instead:
update : Msg -> Model -> ( Model, Cmd Msg )
This is the flow for Html.beginnerProgram:
The Html.beginnerProgram
is used for simple app, so let’s focus on Html.program
.
Looking at the function signature, ( Model, Cmd Msg )
means that the updated function return a tuplet where
- the first element is an updated model
- the second element is a command (Cmd) that, after being executed return a message (Msg)
In the Elm Architecture there are other things that return messages:
- view signature: Model -> Html Msg
— subscriptions signature: Model -> Sub Msg
— update signature: Msg -> Model -> (Model, Cmd Msg)
with the Html.program the flow has two new branches, Sub and Cmd. Sub are coming from subscriptions
, Cmd are coming from the new update
signature:
Commands are usually (always?) used when side effects are involved, for example
- Sending Ajax requests
- Generating random numbers
- Getting date and time
- Save something in the local store
- Using Web Sockets
So the problem running a History Playback is: should we execute also the commands or just follow the messages thread?
As mentioned earlier the “History Explorer” is not replaying commands to avoid undesirable side effects.
Moreover Commands are generating Messages so if we replay both Messages and Commands and then Commands generate messages again, the same messages will arrive two times. Which one should we choose?
Scrolling the page is one of these things that involve side effects (setting the page to a certain scrolling position).
At the moment is only possible to store the scrolling position in the model but is not possible replaying it during the history playback.
To give you and idea of what could be the replay of the interaction, have a look at these examples (click on the History Explorer at the bottom right of the screen and move back and forward along the list) :
- https://lucamug.github.io/elm-image-zoom/
- https://lucamug.github.io/elm-scroll-resize-events/
- https://lucamug.github.io/elm-events-testing/
Possible solutions
I see couple of possible solutions to this issue, but I am sure there are more
1. History Wrapper
Create a wrapper of the update function that would also execute the commands, following some rule. For example we could have these two messages:
ScrollTo Float
(this scroll the page to a certain position, using a port orOnScroll Float
(this save the scrolling position to the model)
During the replay, we should
- ignore the
ScrollTo
- convert the
OnScroll
toScrollTo
2. Simulate the scrolling with some css attribute
For example using CSS transformation “translate”. In this case the app need to be aware when it is in “replay mode” and apply these styles only in this situation.
More about History wrapper
I experimented with the History Wrapper by Ilias Van Peer, but no luck also there because again the wrapper is not replaying the commands.
Saving the history
During the visitor interaction, Msg
are stored in a list.
This is achieved wrapping the view inside the wrapper view with
wrapperView ... =
div []
[ view model |> Html.map Wrapped
, ...
]
Where
view
andmodel
are the view and model from the original appWrapped
is aMsg
in the wrapper app
So messages are converted, for example, from
Increment
to
Wrapped Increment
In the wrapper update, when a Wrapped message arrive it is added to a list.
Commands instead are just sent to the Elm Runtime, as they usually are, but they are not saved in the history.
This is the flow of data during the execution of the wrapped app:
Replaying the history
During the history playback, the Cmd generated by update
are not sent to the Elm Runtime but just discarded.
The core of history playback is:
foldl (\msg model -> update msg model |> Tuple.first) init msgs
The signature of foldl
is
foldl : (a -> b -> b) -> b -> List a -> b
where
a
isMsg
b
(the accumulator) isModel
(a -> b -> b)
is the updated function in its Html.beginnerProgram format:(Msg -> Model -> Model)
and
msgs
is a list of Msginit
is the model in its initial stateupdate
is the update function
Tuple.first
take the return value from the model, that is in the Html.program
format: Msg -> Model -> ( Model, Cmd Msg )
and extract only the Model part, ignoring Cmd Msg
.
So basically this history wrapper downgrade the Html.program
to Html.beginnerProgram
during the replay.
This give us also another interesting insight:
The Elm Architecture (Model Update View) is as simple as one function (Update) going through a fold function where the Model is the accumulator.
The flow of data is
What would happen if during the playback we send Cmd to the Elm Runtime?
This would be the situation:
As you can see, if we ask the Elm Runtime to execute commands, there will be a conflict of Msg, one coming from History Wrapper and one coming from Elm Runtime.
More about Debuggers
Jinjor built an alternative debugger called elm-time-travel
, that can easily wrap your app, just using TimeTravel.program
instead of Html.program
. Examples here.
This is the Mario Debugger.
Conclusion
Replaying users interaction would be useful for tracking bugs, improving usability and many other purposes. Elm is already doing a great job recording the interaction with its History Explorer
. Hopefully in the future there would be some tool that could make the recording and playing of the interaction even better.