Updating a record from a text input

136 Views Asked by At

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:

  • don't hook up setEvent of the input field and the textInput_input event to the same Dynamic. This did not help
  • set setValue to eClient instead of eUpdatedClient - this is the Event Client that we are receiving from outside (e.g. when a row in a table is clicked). Did not help.
  • trigger the Dynamic update from textInput_keypress instead of textInput_input again to avoid a potential loop (although I think this is not the case here. Did not help.

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.

1

There are 1 best solutions below

0
On BEST ANSWER

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