meiersi / blaze-react

A blaze-html style ReactJS binding for Haskell using GHCJS

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Add support for life cycle events

basvandijk opened this issue · comments

In our app we need to know the dimensions of a specific HTML node after it's inserted into the native DOM. React provides Life Cycle Methods (specifically the componentDidUpdate) to support this.

In LumiGuide@bf9a35e @roelvandijk and I made an initial implementation for this.

We don't think we have the right design yet but at the moment it works for us. So see this as a way to start the design discussion.

API

  • We added the event handler registration function:
Text.Blaze.Event.onDomDidUpdate :: (DomNode -> act) -> Attribute act
  • DomNode is a partial representation of the actual DOM node that was updated:
data DomNode =
     DomNode
     { domNodeClassName :: !T.Text
     , domNodeId        :: !T.Text
     , domNodeTagName   :: !T.Text
     , domNodeBoundingClientRect :: !DomRect
     } deriving (Eq, Show)

data DomRect =
     DomRect
     { domRectBottom :: !Int
     , domRectHeight :: !Int
     , domRectLeft   :: !Int
     , domRectRight  :: !Int
     , domRectTop    :: !Int
     , domRectWidth  :: !Int
     } deriving (Eq, Show)

I'm not sure this representation is ideal since we are deciding for the user which properties of the actual DOM node we put in DomNode. It might be better to have:

Text.Blaze.Event.onDomDidUpdate :: (JsObject DOMNode_ -> act) -> Attribute act

so that the user can query (and manipulate!) the actual DOM node. This querying and manipulation does have to be performed in IO so users are required to use tell [...] for this.

  • Applying the action resulting from onDomDidUpdate causes another onDomDidUpdate to fire. This causes an infinite loop. To solve this we would like the user to have control over when the virtual DOM gets updated. For this we added the shouldUpdateDom transition function:
shouldUpdateDom :: Bool -> TransitionM s a

This directly corresponds to shouldComponentUpdate.

When the user calls: shouldUpdateDom False in his transition (and isn't followed by a shouldUpdateDom True) the virtual DOM won't get updated.

Implementation

When the user calls onDomDidUpdate like:

someNode ! onDomDidUpdate handler $ ...

someNode will gain the attribute data-life-cycle-id=<fresh life-cycle id> and we remember the "<fresh life-cycle id> -> handler" association. This happens in Text.Blaze.Renderer.ReactJS.

When we receive an update event for the root blaze-react component we loop over all the "life-cycle id -> handler" associations, lookup the DOM node with the corresponding life-cycle id, construct a DomNode and apply the handler to it.

Since blaze-react is using a single component to represent the entire app it means that this component will receive all update events. It would be better to give the user access to the component API so that he can control the depth of each component.

We modified TransitionM to be:

type TransitionM state action = WriterT [IO action] (WriterT (Last Bool) (State state)) ()

The Last Bool records the desired shouldUpdateDom behavior.

We went with a nested WriterT implementation instead of a single WriterT layer with a tuple so that we didn't need to modify existing calls to tell in client code (we have quite a few of them).

Hi Bas,

you highlight an interesting problem. I currently do not have an
opinion/idea on how to solve it properly. I'm currently not working on
blaze-react. So I'm glad that found a solution that works for you.

Best regards,
Simon
Am 10.03.2015 16:35 schrieb "Bas van Dijk" notifications@github.com:

In our app we need to know the dimensions of a specific HTML node after
it's inserted into the native DOM. React provides Life Cycle Methods
http://facebook.github.io/react/docs/component-specs.html#lifecycle-methods
(specifically the componentDidUpdate) to support this.

In LumiGuide/blaze-react@bf9a35e
LumiGuide@bf9a35e
@roelvandijk https://github.com/roelvandijk and I made an initial
implementation for this.

We don't think we have the right design yet but at the moment it works for
us. So see this as a way to start the design discussion.
API

  • We added the event handler registration function:

Text.Blaze.Event.onDomDidUpdate :: (DomNode -> act) -> Attribute act

  • DomNode is a partial representation of the actual DOM node that
    was updated:

data DomNode =
DomNode
{ domNodeClassName :: !T.Text
, domNodeId :: !T.Text
, domNodeTagName :: !T.Text
, domNodeBoundingClientRect :: !DomRect
} deriving (Eq, Show)
data DomRect =
DomRect
{ domRectBottom :: !Int
, domRectHeight :: !Int
, domRectLeft :: !Int
, domRectRight :: !Int
, domRectTop :: !Int
, domRectWidth :: !Int
} deriving (Eq, Show)

I'm not sure this representation is ideal since we are deciding for the
user which properties of the actual DOM node we put in DomNode. It might
be better to have:

Text.Blaze.Event.onDomDidUpdate :: (JsObject DOMNode_ -> act) -> Attribute act

so that the user can query (and manipulate!) the actual DOM node. This
querying and manipulation does have to be performed in IO so users are
required to use tell [...] for this.

  • Applying the action resulting from onDomDidUpdate causes another
    onDomDidUpdate to fire. This causes an infinite loop. To solve this we
    would like the user to have control over when the virtual DOM gets updated.
    For this we added the shouldUpdateDom transition function:

shouldUpdateDom :: Bool -> TransitionM s a

This directly corresponds to shouldComponentUpdate
http://facebook.github.io/react/docs/component-specs.html#updating-shouldcomponentupdate
.

When the user calls: shouldUpdateDom False in his transition (and isn't
followed by a shouldUpdateDom True) the virtual DOM won't get updated.
Implementation

When the user calls onDomDidUpdate like:

someNode ! onDomDidUpdate handler $ ...

someNode will gain the attribute data-life-cycle-id=
and we remember the " -> handler" association. This
happens in Text.Blaze.Renderer.ReactJS
https://github.com/LumiGuide/blaze-react/blob/bf9a35e47802af1251530d00917807275367ff18/src/Text/Blaze/Renderer/ReactJS.hs#L139
.

When we receive an update event for the root blaze-react component we loop
over
https://github.com/LumiGuide/blaze-react/blob/bf9a35e47802af1251530d00917807275367ff18/src/Blaze/React/Run/ReactJS.hs#L172
all the "life-cycle id -> handler" associations, lookup the DOM node with
the corresponding life-cycle id, construct a DomNode and apply the handler
to it.

Since blaze-react is using a single component to represent the entire app
it means that this component will receive all update events. It would be
better to give the user access to the component API so that he can control
the depth of each component.

We modified TransitionM to be:

type TransitionM state action = WriterT [IO action](WriterT %28Last Bool%29 %28State state%29) ()

The Last Bool records the desired shouldUpdateDom behavior.

We went with a nested WriterT implementation instead of a single WriterT
layer with a tuple so that we didn't need to modify existing calls to tell
in client code (we have quite a few of them).


Reply to this email directly or view it on GitHub
#16.

Hi @basvandijk,

I don't know whether this would be harder to implement, but from an API point of view it seems like it would be best to expose this via a request.

domNodeRequest
    :: (DomNode -> act)
       -- ^ A wrapper for the node info
    -> String
       -- ^ The ID of the element you're looking for
    -> IO act

Say you need the DOM info for element "#foo". When you move into a state which includes "#foo" in its view, you submit a domNodeRequest. The next action to be submitted should contain the DomNode for "#foo".

By the way, in blaze-react-0.2, you would implement this as a service:

module Blaze.Core.Service.DomNode
    ( DomNodeR(..)
    , DomNodeA(..)
    ) where

newtype DomNodeR = GetNodeInfo String
newtype DomNodeA = ReturnNodeInfo DomNode
module Blaze.ReactJS.Service.DomNode
    ( handleRequest
    ) where

handleRequest :: (DomNodeA -> IO ()) -> DomNodeR -> IO ()
handleRequest channel request = ...