I have a modal dialog that allows to “edit” something from the parent component. Now, I want this “something” to always be fetched from the parent component. This seems to be a no-brainer, because when parent changes data, that causes the modal to re-render with new data.
But the real problem is when the parent wasn’t changed, and a user modified some data, then dismissed the dialog. In this case the dialog caches the modified data, so the next time it pops up, the modified data is shown instead of the actual data from the parent.
So, how to can I make sure that every time a modal window pops up it’s re-rendered anew?
Steps to reproduce
Given this src/Main.purs that just implements a modal dialog with a <textarea>
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, null)
import Effect (Effect)
import Effect.Class.Console (log)
import Effect.Exception (throw)
import Effect.Uncurried (runEffectFn1)
import React.Basic.DOM (css)
import React.Basic.DOM as R
import React.Basic.DOM.Client (createRoot, renderRoot)
import React.Basic.DOM.Events as RE
import React.Basic.Events as RE
import React.Basic.Hooks (Component, JSX, Ref, component, readRefMaybe)
import React.Basic.Hooks as React
import Unsafe.Coerce (unsafeCoerce)
import Web.DOM.Internal.Types (Node)
import Web.DOM.NonElementParentNode (getElementById)
import Web.Event.Event (Event)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)
import FFI (closeDialogIfItsTarget, showModal)
type RefNode = Ref (Nullable Node)
main :: Effect Unit
main = do
doc <- document =<< window
root <- getElementById "root" $ toNonElementParentNode doc
case root of
Nothing -> throw "Could not find root."
Just container -> do
reactRoot <- createRoot container
app <- mkApp
renderRoot reactRoot (app {})
mkApp :: Component {}
mkApp = do
component "Test" \_ -> React.do
modalRef :: RefNode <- React.useRef null
pure $
R.div_ [ modalDialog modalRef [R.textarea {defaultValue: "hello!"}]
, R.button { onClick: RE.handler_ $ applyToReactRef modalRef showModal
, children: [R.text "Launch modal"]}
]
applyToReactRef :: RefNode -> (Node -> Effect Unit) -> Effect Unit
applyToReactRef mbRef f = readRefMaybe mbRef >>= case _ of
Nothing -> log "Error: null node"
Just ref -> f ref
modalDialog :: RefNode -> Array JSX -> JSX
modalDialog dialogRef children =
let
closeOnClick :: Event -> Effect Unit
closeOnClick ev = applyToReactRef dialogRef (closeDialogIfItsTarget ev)
-- The oddness with styles is to make sure that clicking outside the dialog will
-- close it. Source
-- https://stackoverflow.com/questions/25864259/how-to-close-the-new-html-dialog-tag-by-clicking-on-its-backdrop
in
R.dialog { style: css {padding: "0"}
, onClick: RE.handler RE.nativeEvent closeOnClick
, ref: dialogRef
, children:
[ R.form
{ style: css {padding: "1rem"}
, method: "dialog"
, children
}
]
}
I just noticed, it also seems like the React lib forgot to implement onClose event (which is why I guess you’re using this “addEventListener”). I sent a fix
So, while the PR adding the onClick event wasn’t merged, I tried your code with EventTarget and co, but found it too involved and complicated. There’s lots of code in useEffectOnce that may be avoided. (I also presume it should be useEffect, because the …Once version would be executed just once, and when it will happen the dialogRef wasn’t assigned yet, so the mempty branch should get executed instead of the assignment).
Instead I figured it’s way simpler to re-declare dialog creation with the onClose event added, like:
-- A `dialog` JSX that allows to pass onClose field that is missing in the React
-- dialog. A fix was sent
-- https://github.com/purescript-react/purescript-react-basic-dom/pull/55
dialogRaw :: ∀ attrs. Record attrs -> JSX
dialogRaw = React.element (unsafeCoerce (unsafePerformEffect (R.unsafeCreateDOMComponent "dialog")))
Full implementation for the original code (barring the reset FFI):
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Data.Nullable (Nullable, null)
import Data.Tuple (Tuple(..))
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Class.Console (log)
import Effect.Exception (throw)
import Effect.Unsafe (unsafePerformEffect)
import FFI (closeDialogIfItsTarget, showModal, formReset)
import React.Basic.DOM (css)
import React.Basic.DOM as R
import React.Basic.DOM.Client (createRoot, renderRoot)
import React.Basic.DOM.Events as RE
import React.Basic.Events as RE
import React.Basic.Hooks (Component, JSX, Ref, component, readRefMaybe)
import React.Basic.Hooks as React
import Unsafe.Coerce (unsafeCoerce)
import Web.DOM.Internal.Types (Node)
import Web.DOM.NonElementParentNode (getElementById)
import Web.Event.Event (Event)
import Web.HTML (window)
import Web.HTML.HTMLDocument (toNonElementParentNode)
import Web.HTML.Window (document)
type RefNode = Ref (Nullable Node)
-- A `dialog` JSX that allows to pass fields that are missing in the React dialog
dialogRaw :: ∀ attrs. Record attrs -> JSX
dialogRaw = React.element (unsafeCoerce (unsafePerformEffect (R.unsafeCreateDOMComponent "dialog")))
main :: Effect Unit
main = do
doc <- document =<< window
root <- getElementById "root" $ toNonElementParentNode doc
case root of
Nothing -> throw "Could not find root."
Just container -> do
reactRoot <- createRoot container
app <- mkApp
renderRoot reactRoot (app {})
mkApp :: Component {}
mkApp = do
modalDialog <- mkModalDialog
component "Test" \_ -> React.do
modalRef :: RefNode <- React.useRef null
pure $
R.div_ [ modalDialog $ modalRef /\ [R.textarea {defaultValue: "hello!"}]
, R.button { onClick: RE.handler_ $ applyToReactRef modalRef showModal
, children: [R.text "Launch modal"]}
]
applyToReactRef :: RefNode -> (Node -> Effect Unit) -> Effect Unit
applyToReactRef mbRef f = readRefMaybe mbRef >>= case _ of
Nothing -> log "Error: null node"
Just ref -> f ref
-- | A dialog window. NOTE: this dialog impelemnts a form, and you're not allowed to
-- | embed a form into a form.
mkModalDialog :: React.Component (Tuple RefNode (Array JSX))
mkModalDialog =
React.component "modalDialog" \(Tuple dialogRef children) -> React.do
formRef :: RefNode <- React.useRef null
let
closeOnClick :: Event -> Effect Unit
closeOnClick = applyToReactRef dialogRef <<< closeDialogIfItsTarget
resetFormOnClose :: Effect Unit
resetFormOnClose = applyToReactRef formRef formReset
-- The oddness with styles is to make sure that clicking outside the dialog will
-- close it. Source
-- https://stackoverflow.com/questions/25864259/how-to-close-the-new-html-dialog-tag-by-clicking-on-its-backdrop
-- TODO: paths handling has to be separated to its own component for better modal
-- dialog reuse and logic incapsulation
pure $
dialogRaw { style: css {padding: "0"}
, onClick: RE.handler RE.nativeEvent closeOnClick
, onClose: RE.handler_ resetFormOnClose
, ref: dialogRef
, children:
[ R.form
{ style: css {padding: "1rem"}
, method: "dialog"
, children
, ref: formRef
}
]
}