haskellreflexreflex-dom

Updating a record from a text input


relative Haskell and reflex noob here. Decided to get my feet wet with a real-world application.

I am having a problem with triggering an update to a Dynamic containing my record once the user enters text in a textInput.

The code compiles in GHCJS, but once I open up the web page it shows up blank. If I remove the line marked as problematic (which creates the update event) it works fine (i.e. setting the record from eClient and from the clear button works).

data Client = Client
        { _clientName :: Text
        , _contacts :: [Text] -- TODO make a new type for this
        , _balance :: Int -- this is calculated
        , _notes :: [Text] -- free text notes, might come in handy
        } deriving (Show, Eq)

updateFieldFromTextInput :: Reflex t =>
                            (Client -> T.Text -> Client) ->
                            Dynamic t Client ->
                            Event t T.Text ->
                            Event t Client
updateFieldFromTextInput setter dynClient evInput = attachPromptlyDynWith setter dynClient evInput

-- the input event is the one to set a client on the widget
-- the output event is when a client is saved
clientEditWidget :: MonadWidget t m => Event t Client -> m (Event t Client)
clientEditWidget eClient = mdo
  (editClient, eSaveButton) <- elClass "div" "client-edit" $ mdo

    -- fires an Event t Client when the input field is changed
    let eNameInput = (nameInput ^. textInput_input)
        nameSetter = flip (clientName .~)
        eNameUpdate = updateFieldFromTextInput nameSetter editClient eNameInput
        eClear = mkClient "" <$ eClearButton
        eClientReplaced = leftmost [eClient, eClear]
        eClientModified = leftmost [eNameUpdate]

    -- the currently edited client
    -- using eClientModified causes a blank screen
    -- editClient <- holdDyn (mkClient "") eClientModified
    editClient <- holdDyn (mkClient "") eClientReplaced

    -- lay out the widgets
    text "edit client"
    nameInput <- textInput $
                 def & setValue .~
                 ((view clientName) <$> eClientReplaced)

    contactsInput <- textArea $
                     def & setValue .~
                     ((T.concat . view contacts) <$> eClientReplaced)
    eSaveButton <- button "Save"
    eClearButton <- button "Clear"
    dynText =<< holdDyn "updated client will appear here" (T.pack . show <$> eClientModified)
    return (editClient, eSaveButton)
  return $ tagPromptlyDyn editClient eSaveButton

Edit: I thought I might be introducing an infinite loop somewhere, so tried a couple of things:

An infinite loop may well be the problem, though.

Edit: Added another dynText which shows that the event eClientModified fires a perfectly good Client. So it is really in updating the editClient Dynamic that it fails.


Solution

  • Found the cause of my problem in the docs of tagDyn, ultimately: "Additionally, this means that the output Event may not be used to directly change the input Dynamic, because that would mean its value depends on itself. When creating cyclic data flows, generally tag (current d) e is preferred."

    Somehow I expected this to magically work...

    So, using the Behavior for the update event instead of the Dynamic (and attachWith instead of attachPromptlyDynWith) works fine.

    Here is the working code:

    updateFieldFromTextInput :: Reflex t =>
                                (Client -> T.Text -> Client) ->
                                Behavior t Client ->
                                Event t T.Text ->
                                Event t Client
    updateFieldFromTextInput setter bClient evInput = attachWith setter bClient evInput
    
    -- the input event is the one to set a client on the widget
    -- the output event is when a client is saved
    clientEditWidget :: MonadWidget t m => Event t Client -> m (Event t Client)
    clientEditWidget eClient = mdo
      (editClient, eSaveButton) <- elClass "div" "client-edit" $ mdo
    
        -- fires an Event t Client when the input field is changed
        let eNameInput = (nameInput ^. textInput_input)
            nameSetter = flip (clientName .~)
            eNameUpdate = updateFieldFromTextInput nameSetter (current editClient) eNameInput
            eClear = mkClient "" <$ eClearButton
            eClientReplaced = leftmost [eClient, eClear]
            eClientModified = leftmost [eNameUpdate]
    
        -- the currently edited client
        editClient <- holdDyn (mkClient "") eClientModified
    
        -- lay out the widgets
        text "edit client"
        nameInput <- textInput $
                     def & setValue .~
                     ((view clientName) <$> eClientReplaced)
    
        contactsInput <- textArea $
                         def & setValue .~
                         ((T.concat . view contacts) <$> eClientReplaced)
        eSaveButton <- button "Save"
        eClearButton <- button "Clear"
        dynText =<< holdDyn "updated client will appear here" (T.pack . show <$> eClientModified)
        return (editClient, eSaveButton)
      return $ tagPromptlyDyn editClient eSaveButton