Add timestamp also when updating a record

Hi All!

I still have problem figuring out how to add a timestamp too when I update a record in Elm.
I’ve seen many examples of performing Tasks, using andThen, or even batch Commands, so I guess there should be the answer somewhere. However, I have no idea how to use a Task in this basic example to add a real posix timestamp instead of zero in the below example.

This example works with Elm 0.19.1. Just uses 0 as a timestamp.

module Main exposing (main)

import Browser
import Html exposing (Html, button, div, input, li, p, text, ul)
import Html.Attributes exposing (value)
import Html.Events exposing (onClick, onInput)


main : Program () Model Msg
main =
    Browser.element
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }


type alias Item =
    { name : String
    , timestamp : Int
    }


type alias Model =
    { textField : String
    , items : List Item
    }


type Msg
    = TextChange String
    | Add


init : flags -> ( Model, Cmd Msg )
init _ =
    ( { textField = "", items = [] }
    , Cmd.none
    )


subscriptions : Model -> Sub Msg
subscriptions _ =
    Sub.none


view : Model -> Html Msg
view model =
    div []
        [ input [ onInput TextChange, value model.textField ] []
        , button [ onClick Add ] [ text "Add" ]
        , ul [] (List.map renderItem model.items)
        ]


renderItem : Item -> Html Msg
renderItem item =
    li []
        [ p [] [ text item.name ]
        , p [] [ text <| String.fromInt item.timestamp ]
        ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        TextChange text ->
            ( { model | textField = text }, Cmd.none )

        Add ->
            ( { model
                | items = model.items ++ [ Item model.textField 0 ] -- TODO Use real timestamp instead of zero
                , textField = ""
              }
            , Cmd.none
            )

Can you show me how to extend this code to end up using a real timestamp in the Item?

Thanks!

Here is an example https://ellie-app.com/h2kgDqczNGJa1 of how I’d approach this. There are likely a few others ways to do this as well, and it may change as your app changes.

3 Likes

Hi!
In my opinion, and if you don’t require absolute precision in the timestamps, the simplest way to achieve what you’re looking for is to:

  1. add a Subscription that sends a Msg every X milliseconds, e.g.:
subscriptions _ =
    Time.every 100 ReceivedTime

type Msg
    = YourMessages
    | ReceivedTime Time.Posix

update msg model =
    case msg of
        YourMessages ->
            (model, Cmd.none)
        ReceivedTime time ->
            ({ model | currentTime = time }), Cmd.none)
  1. use the timestamp currently stored in the model when you do your update.

There are other ways, such as making it so that when users request an update to your model, you instead create a Task that will get the current time and perform the update, but it seems needlessly complicated and may mess up the order of your updates. One of the drawbacks of the solution I’ve suggested is that you’re going to have a lot of Msgs getting in your update function.

EDIT: @wolfadex’s way of doing this (updating the timestamp as an “afterthought” rather than as a “prerequisite”) seems to invalidate my concerns about order of operation, at the cost of some latency in the timestamping. For the usage that he showcased (a “last modified” timestamp), this does not matter at all and seems like a great way of doing things =)

2 Likes

Thanks! My problem with this solution is that the information written in the model at the time of the Increment and Decrement messages does not contain the timestamp. That is only triggered as a separate command. And when it finishes, I don’t have the information about how the model was updated. So I cannot bind them together.

So I’m rather looking for a solution where I can bind the timestamp to the actual data updated in the model. In your solution they are pretty separated. If “count” would be a record instead of a number, how would you also add the timestamp at the time when you update other fields?

Or is this something that is not the Elm-way? Should I think differently in this world?

Hi! In this concrete example I can use this kind of solution. It solves my specific problem of having timestamps, and I don’t need more accuracy. However, I still fear that I have this kind of problem that I have no idea how to solve, and I may bump into it next time in a different shape.

You’re most probably right, that I would just overcomplicate my solution with more messages coming and going about the timestamps. I just come from an imperative world where it is quite trivial to ask for a timestamp and use it. I have a feeling that in Elm, some hard problems have very simple solutions, while some otherwise simple problems have more complicated solutions. I just need to find the balance.

I had a similar problem where I needed timestamps for most cases in my update function. I used this approach:


type Msg
  = NameChanged String
  | SomeOtherThingChanged String
  | MsgWithTimestamp Msg Time.Posix

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    MsgWithTimestamp subMsg timestamp ->
      updateWithTimestamp timestamp subMsg model

    _ ->
      ( model
      , Time.now
        |> Task.perform (MsgWithTimestamp msg)
      )

updateWithTimestamp : Time.Posix -> Msg -> Model -> ( Model, Cmd Msg )
updateWithTimestamp time msg model =
  case msg of
    ....

9 Likes

@albertdahlin, that’s it! This is what I was looking for :slight_smile:

And this solution is very general, I can use it for different purpuses when some message chaining is needed. Thank you very much!

Just one correction, the default branch should return a (Model, Cmd Msg) like this

    _ ->
        ( model
        , Time.now
            |> Task.perform (MsgWithTimestamp msg)
        )

Otherwise, it works perfectly.

Thanks for the correction, I’ve updated my post.

If every Msg needs to be timestamped, then this approach can be made fully generic:

module Timestamped exposing (Timestamped(..), element)

import Browser
import Time
import Html exposing (Html)
import Task


type Timestamped msg
    = NeedsTimestamp msg
    | Timestamped msg Time.Posix


element :
    { init : flags -> ( model, Cmd msg )
    , update : Time.Posix -> msg -> model -> ( model, Cmd msg )
    , view : model -> Html msg
    , subscriptions : model -> Sub msg
    }
    -> Program flags model (Timestamped msg)
element { init, update, view, subscriptions } =
    Browser.element
        { init = init >> Tuple.mapSecond (Cmd.map NeedsTimestamp)
        , update =
            \msg model ->
                case msg of
                    NeedsTimestamp innerMsg ->
                        ( model, Task.perform (Timestamped innerMsg) Time.now )

                    Timestamped innerMsg timestamp ->
                        update timestamp innerMsg model |> Tuple.mapSecond (Cmd.map NeedsTimestamp)
        , view = view >> Html.map NeedsTimestamp
        , subscriptions = subscriptions >> Sub.map NeedsTimestamp
        }

This assumes that you are using Browser.element, but equivalent “wrapper functions” could be made for any other Program type. This way, the only change to your standard functions is the Time.Posix parameter to the update function; everything continues to use your own Msg type, and it becomes impossible to even represent a message with multiple timestamps (MsgWithTimestamp (MsgWithTimeStamp (NameChanged newName) innerTimestamp) outerTimestamp).

3 Likes

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.