Skip to content

Commit

Permalink
feat: check extra attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
srghma committed Jun 4, 2020
1 parent 3fb49b1 commit b98e648
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 69 deletions.
2 changes: 1 addition & 1 deletion src/Halogen/VDom/Attributes.purs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ foreign import attributes ∷ DOM.Element → NamedNodeMap
forEachE
EFn.EffectFn2
NamedNodeMap
(EFn.EffectFn1 String Unit)
(EFn.EffectFn1 { name :: String } Unit)
Unit
forEachE = unsafeCoerce Util.forEachE
3 changes: 3 additions & 0 deletions src/Halogen/VDom/DOM.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ module Halogen.VDom.DOM
, hydrateVDom
) where

import Prelude
import Halogen.VDom.DOM.Elem (buildElem, hydrateElem)
import Halogen.VDom.DOM.Keyed (buildKeyed, hydrateKeyed)
import Halogen.VDom.DOM.Text (buildText, hydrateText)
import Halogen.VDom.DOM.Types (VDomMachine, VDomSpec)
import Halogen.VDom.DOM.Widget (buildWidget, hydrateWidget)
import Halogen.VDom.Util (warnAny)

import Effect.Uncurried as EFn
import Halogen.VDom.DOM.Elem (buildElem) as Export
Expand All @@ -24,6 +26,7 @@ hydrateVDom spec rootNode = hydrate rootNode
where
build = buildVDom spec
hydrate node = EFn.mkEffectFn1 \vdom -> do
EFn.runEffectFn2 warnAny "Path" { node, vdom }
case vdom of
Text s → EFn.runEffectFn5 hydrateText node spec hydrate build s
Elem namespace elemName attribute childrenVdoms → EFn.runEffectFn8 hydrateElem node spec hydrate build namespace elemName attribute childrenVdoms
Expand Down
29 changes: 5 additions & 24 deletions src/Halogen/VDom/DOM/Prop.purs
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,20 @@ module Halogen.VDom.DOM.Prop
, hydrateProp
) where

import Data.String.Common (joinWith)
import Prelude
import Halogen.VDom.DOM.Prop.Implementation (applyProp, diffProp, hydrateApplyProp, mbEmit, removeProp)
import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropState, BuildPropFunction)
import Halogen.VDom.DOM.Prop.Utils (propToStrKey)
import Halogen.VDom.Util (STObject')
import Prelude (Unit, bind, discard, pure, unit, when, (#), ($), (<>), (>))

import Data.Function.Uncurried as Fn
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Exception (error, throwException)
import Effect.Uncurried as EFn
import Foreign.Object as Object
import Halogen.VDom.Attributes (attributes, forEachE) as Attributes
import Halogen.VDom.DOM.Prop.Types (Prop(..), ElemRef(..), PropValue, propFromString, propFromBoolean, propFromInt, propFromNumber) as Export
import Halogen.VDom.Machine (Step, Step'(..), mkStep)
import Halogen.VDom.Set as Set
import Halogen.VDom.Util as Util
import Web.DOM.Element (Element) as DOM

-- inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030
mkExtraAttributeNames DOM.Element Effect (Set.Set String)
mkExtraAttributeNames el = do
let
namedNodeMap = Attributes.attributes el

(set Set.Set String) ← Set.empty
EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \name → EFn.runEffectFn2 Set.add name set)
pure set

throwErrorIfExtraAttributeNamesNonEmpty Set.Set String Effect Unit
throwErrorIfExtraAttributeNamesNonEmpty extraAttributeNames = do
when (Set.size extraAttributeNames > 0)
(do
throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ")
)
import Halogen.VDom.DOM.Prop.Checkers (mkExtraAttributeNames, checkExtraAttributeNamesIsEmpty)

hydrateProp
a
Expand All @@ -53,6 +31,9 @@ hydrateProp emit el = renderProp
extraAttributeNames ← mkExtraAttributeNames el

(props Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn4 hydrateApplyProp extraAttributeNames el emit events)

checkExtraAttributeNamesIsEmpty extraAttributeNames el

let
(state PropState a) =
{ events: Util.unsafeFreeze events
Expand Down
46 changes: 32 additions & 14 deletions src/Halogen/VDom/DOM/Prop/Checkers.purs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
module Halogen.VDom.DOM.Prop.Checkers where

import Halogen.VDom.DOM.Prop.Types (PropValue)
import Halogen.VDom.DOM.Prop.Utils (unsafeGetProperty)
import Prelude (Unit, bind, pure, unit, ($), (<#>), (<>), (==), discard)
import Prelude

import Data.Function.Uncurried as Fn
import Data.Maybe (Maybe(..))
import Data.Nullable (toMaybe, toNullable)
import Data.String (toLower)
import Data.String.Common (joinWith)
import Effect (Effect)
import Effect.Exception (error, throwException)
import Effect.Uncurried as EFn
import Halogen.VDom.Attributes (attributes, forEachE) as Attributes
import Halogen.VDom.DOM.Prop.Types (PropValue)
import Halogen.VDom.DOM.Prop.Utils (unsafeGetProperty)
import Halogen.VDom.Set as Set
import Halogen.VDom.Types (ElemName(..), Namespace)
import Halogen.VDom.Util (anyToString, fullAttributeName, quote, warnAny)
import Halogen.VDom.Util as Util
import Web.DOM.Element (Element) as DOM
import Effect.Exception (error, throwException)

checkAttributeExistsAndIsEqual Maybe Namespace String String DOM.Element Effect Unit
checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue element = do
Expand All @@ -23,17 +27,31 @@ checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue
EFn.runEffectFn2 warnAny "Error at " { element }
throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it is missing"
Just elementValue' →
if elementValue' == expectedElementValue
then pure unit
else do
EFn.runEffectFn2 warnAny "Error at " { element }
throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it was equal to " <> quote elementValue'
unless (elementValue' == expectedElementValue) (do
EFn.runEffectFn2 warnAny "Error at " { element }
throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it was equal to " <> quote elementValue'
)

checkPropExistsAndIsEqual String PropValue DOM.Element Effect Unit
checkPropExistsAndIsEqual propName expectedPropValue element = do
let propValue = Fn.runFn2 unsafeGetProperty propName element
if Fn.runFn2 Util.refEq propValue expectedPropValue
then pure unit
else do
EFn.runEffectFn2 warnAny "Error at " { element }
throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue)
unless (Fn.runFn2 Util.refEq propValue expectedPropValue) (do
EFn.runEffectFn2 warnAny "Error at " { element, expectedPropValue }
throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue)
)

-- | Inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030
mkExtraAttributeNames DOM.Element Effect (Set.Set String)
mkExtraAttributeNames el = do
let
namedNodeMap = Attributes.attributes el
(set Set.Set String) ← Set.empty
EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \attribute → EFn.runEffectFn2 Set.add (toLower attribute.name) set)
pure set

checkExtraAttributeNamesIsEmpty Set.Set String -> DOM.Element -> Effect Unit
checkExtraAttributeNamesIsEmpty extraAttributeNames element =
when (Set.size extraAttributeNames > 0) (do
EFn.runEffectFn2 warnAny "Error at " { element }
throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ")
)
28 changes: 18 additions & 10 deletions src/Halogen/VDom/DOM/Prop/Implementation.purs
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
module Halogen.VDom.DOM.Prop.Implementation where

import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..))
import Halogen.VDom.DOM.Prop.Checkers (checkAttributeExistsAndIsEqual, checkPropExistsAndIsEqual)
import Halogen.VDom.DOM.Prop.Utils (removeProperty, setProperty, unsafeGetProperty)
import Prelude (Unit, bind, discard, pure, unit, (==))

import Data.Function.Uncurried as Fn
import Data.Maybe (Maybe(..))
import Data.Nullable (toNullable)
import Data.String.Common (toLower)
import Data.Tuple (Tuple(..), fst, snd)
import Effect (Effect)
import Effect.Ref as Ref
import Effect.Uncurried as EFn
import Foreign.Object as Object
import Halogen.VDom.DOM.Prop.Checkers (checkAttributeExistsAndIsEqual, checkPropExistsAndIsEqual)
import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..))
import Halogen.VDom.DOM.Prop.Utils (removeProperty, setProperty, unsafeGetProperty)
import Halogen.VDom.Set as Set
import Halogen.VDom.Types (ElemName(..))
import Halogen.VDom.Util (STObject', fullAttributeName)
import Halogen.VDom.Util (STObject', fullAttributeName, warnAny)
import Halogen.VDom.Util as Util
import Prelude (Unit, bind, discard, pure, unit, (==))
import Web.DOM.Element (Element) as DOM
import Web.Event.Event (EventType(..), Event) as DOM
import Web.Event.EventTarget (eventListener, EventListener) as DOM
import Data.String.Common (toLower)
import Halogen.VDom.Set as Set

hydrateApplyProp
a
Expand All @@ -39,8 +38,17 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect
pure v
Property propName val → do
checkPropExistsAndIsEqual propName val el
let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan`
EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames

-- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propName, val, el, extraAttributeNames }

if propName == "className"
then EFn.runEffectFn2 Set.delete "class" extraAttributeNames
else do
let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan`
EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames

-- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual after" { propName, val, el, extraAttributeNames }

pure v
Handler eventType emitterInputBuilder → do
EFn.runEffectFn5 applyPropHandler el emit events eventType emitterInputBuilder
Expand Down
33 changes: 15 additions & 18 deletions src/Halogen/VDom/Finders.purs
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
module Halogen.VDom.Finders where

import Prelude

import Data.Either (Either(..), note)
import Effect (Effect)
import Web.DOM.Element (Element)
import Web.DOM.ParentNode (ParentNode)
import Web.DOM.ParentNode (firstElementChild, childElementCount) as DOM.ParentNode
import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM
import Data.Maybe (maybe)
import Effect.Exception (error, throwException)
import Web.DOM (Element)
import Web.DOM.Element (toParentNode) as DOM.Element

findRequiredElement String ParentNode Effect Element
findRequiredElement selector parentNode =
DOM.querySelector (DOM.QuerySelector selector) parentNode
>>= maybe (throwException $ error $ selector <> " not found") pure
import Web.DOM.ParentNode (firstElementChild, childElementCount) as DOM.ParentNode

-- | Used for hydration
findRootElementInsideOfRootContainer :: Element -> Effect Element
findRootElementInsideOfRootContainer container = do
findElementFirstChild :: Element -> Effect (Either String Element)
findElementFirstChild container = do
let
container' = DOM.Element.toParentNode container

childrenCount <- DOM.ParentNode.childElementCount container'
unless (childrenCount == 1) (throwException $ error $ "Root container should have 1 child element (aka root element; it can be Element, Keyed, Text, etc.), but actual children count is " <> show childrenCount)
rootElement ← DOM.ParentNode.firstElementChild container'
maybe (throwException $ error $ "Root element not found") pure rootElement
where
container' = DOM.Element.toParentNode container

if childrenCount /= 1
then pure $ Left $ "Root container should have only 1 child element (aka root element; it can be Element, Keyed, Text, etc.), but actual children count is " <> show childrenCount
else do
maybeRootElement <- DOM.ParentNode.firstElementChild container'
pure $ note "Root element not found" $ maybeRootElement
16 changes: 14 additions & 2 deletions test/Hydration.purs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ module Test.Hydration where

import Prelude

import Effect.Exception (error, throwException)
import Data.Either (either)
import Data.Newtype (un)
import Data.Tuple (Tuple(..))
import Effect (Effect)
import Effect.Uncurried as EFn
import Halogen.VDom as V
import Halogen.VDom.Finders (findElementFirstChild)
import Web.DOM.ParentNode (ParentNode)
import Web.DOM (Element)
import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM
import Data.Maybe (maybe)
import Halogen.VDom.Util (addEventListener) as Util
import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=))
import Web.Event.EventTarget (eventListener) as DOM
import Web.HTML (window) as DOM
import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM
import Web.HTML.Window (document) as DOM
import Halogen.VDom.Finders (findRequiredElement, findRootElementInsideOfRootContainer)

type State = Array { classes String, text String, key String }

Expand All @@ -40,13 +46,19 @@ renderData stateArray =
[ "className" := elementState.classes ]
[ text elementState.text ]

findRequiredElement String ParentNode Effect Element
findRequiredElement selector parentNode = do
maybeElement <- DOM.querySelector (DOM.QuerySelector selector) parentNode
maybe (throwException $ error $ selector <> " not found") pure maybeElement

main Effect Unit
main = do
win ← DOM.window
doc ← DOM.document win

appDiv ← findRequiredElement "#app" (DOM.toParentNode doc)

rootElement ← findRootElementInsideOfRootContainer appDiv
rootElement ← findElementFirstChild appDiv >>= either (throwException <<< error) pure

updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc)

Expand Down

0 comments on commit b98e648

Please sign in to comment.