From b385b5046643e2f948c71f5c6ce4f432f338b1d6 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 13 May 2020 16:56:27 +0300 Subject: [PATCH 01/48] refactor: add comments, better names for arguments --- .gitignore | 1 + GUIDE.md | 6 +- package.json | 1 + src/Halogen/VDom/DOM.purs | 87 +++++++++++++++------ src/Halogen/VDom/DOM/Prop.purs | 135 +++++++++++++++++++++++++-------- src/Halogen/VDom/Machine.purs | 19 +++++ src/Halogen/VDom/Thunk.purs | 24 +++--- src/Halogen/VDom/Util.js | 61 +++++++-------- src/Halogen/VDom/Util.purs | 54 +++++++------ test/Main.purs | 44 +---------- test/TestVdom.purs | 47 ++++++++++++ 11 files changed, 320 insertions(+), 159 deletions(-) create mode 100644 test/TestVdom.purs diff --git a/.gitignore b/.gitignore index 709fd09..577bc1e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ package-lock.json /bower_components/ /node_modules/ /output/ +/app.js diff --git a/GUIDE.md b/GUIDE.md index 2dfb19e..245cd12 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -14,12 +14,14 @@ render ∷ MyState → MyVDom main = do -- Build the initial machine - machine1 ← V.buildVDom myVDomSpec (render state1) + (machine1 :: VDomMachine a w) ← V.buildVDom myVDomSpec (render state1) - -- Attach the output node to the DOM + -- `machine1` contains a new `DOM.Node` (output node) in it's state + -- Attach that output node to the DOM appendChildToBody (V.extract machine1) -- Patch + -- `V.step` patches previous `DOM.Node` (stored in `machine1`) by running effects machine2 ← V.step machine1 (render state2) machine3 ← V.step machine2 (render state3) ... diff --git a/package.json b/package.json index 29b25d9..7737036 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "clean": "rimraf output && rimraf", "test": "pulp build -I test -- --censor-lib --strict", "build": "pulp build -- --censor-lib --strict" + "spago:test": "spago bundle-app --main Test.Main --to app.js" }, "devDependencies": { "pulp": "^12.3.1", diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 83d7b3a..7eda910 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -25,12 +25,15 @@ import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOMElement import Web.DOM.Node (Node) as DOM +-- A function, that takes `VDom a w` and builds a `DOM.Node` type VDomMachine a w = Machine (VDom a w) DOM.Node type VDomStep a w = Step (VDom a w) DOM.Node type VDomInit i a w = EFn.EffectFn1 i (VDomStep a w) +-- Equal to +-- (VDomSpec a w) -> (VDOM a w -> Step (VDOM a w) DOM.Node) -> i -> Effect (Step (VDOM a w) DOM.Node) type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomStep a w) type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i j k l (VDomStep a w) @@ -38,8 +41,16 @@ type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i -- | Widget machines recursively reference the configured spec to potentially -- | enable recursive trees of Widgets. newtype VDomSpec a w = VDomSpec - { buildWidget ∷ VDomSpec a w → Machine w DOM.Node + { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` + -- example: + + -- buildAttributes = buildProps handler + -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/VDom/Driver.purs#L68-L71 + + -- what is handler + -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 , buildAttributes ∷ DOM.Element → Machine a Unit + -- We need document to be able to call `document.createElement` function , document ∷ DOM.Document } @@ -56,11 +67,11 @@ buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where build = EFn.mkEffectFn1 case _ of - Text s → EFn.runEffectFn3 buildText spec build s + Text s → EFn.runEffectFn3 buildText spec build s -- build text machine Elem ns n a ch → EFn.runEffectFn6 buildElem spec build ns n a ch - Keyed ns n a ch → EFn.runEffectFn6 buildKeyed spec build ns n a ch - Widget w → EFn.runEffectFn3 buildWidget spec build w - Grafted g → EFn.runEffectFn1 build (runGraft g) + Keyed ns n a keyedCh → EFn.runEffectFn6 buildKeyed spec build ns n a keyedCh + Widget w → EFn.runEffectFn3 buildWidget spec build w -- machine that has full control of it's lifecycle + Grafted g → EFn.runEffectFn1 build (runGraft g) -- optimization type TextState a w = { build ∷ VDomMachine a w @@ -71,15 +82,15 @@ type TextState a w = buildText ∷ ∀ a w. VDomBuilder String a w buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do node ← EFn.runEffectFn2 Util.createTextNode s spec.document - let state = { build, node, value: s } + let (state :: TextState a w) = { build, node, value: s } pure $ mkStep $ Step node state patchText haltText patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) -patchText = EFn.mkEffectFn2 \state vdom → do +patchText = EFn.mkEffectFn2 \state newVdom → do let { build, node, value: value1 } = state - case vdom of + case newVdom of Grafted g → - EFn.runEffectFn2 patchText state (runGraft g) + EFn.runEffectFn2 patchText state (runGraft g) -- Before there was a Text on this place. We call patchText instead of patch to be able to remove text Text value2 | value1 == value2 → pure $ mkStep $ Step node state patchText haltText @@ -89,7 +100,7 @@ patchText = EFn.mkEffectFn2 \state vdom → do pure $ mkStep $ Step node nextState patchText haltText _ → do EFn.runEffectFn1 haltText state - EFn.runEffectFn1 build vdom + EFn.runEffectFn1 build newVdom haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit haltText = EFn.mkEffectFn1 \{ node } → do @@ -105,17 +116,28 @@ type ElemState a w = , children ∷ Array (VDomStep a w) } -buildElem ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (VDom a w)) a w +buildElem + ∷ ∀ a w + . VDomBuilder4 + (Maybe Namespace) + ElemName + a + (Array (VDom a w)) + a + w buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let + node :: DOM.Node node = DOMElement.toNode el + + onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn2 \ix child → do - res ← EFn.runEffectFn1 build child + (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 build child EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res children ← EFn.runEffectFn2 Util.forE ch1 onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 + attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 -- build machine that takes attributes let state = { build @@ -133,7 +155,7 @@ patchElem = EFn.mkEffectFn2 \state vdom → do case vdom of Grafted g → EFn.runEffectFn2 patchElem state (runGraft g) - Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do + Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do -- if new vdom is elem AND new and old are equal case Array.length ch1, Array.length ch2 of 0, 0 → do attrs2 ← EFn.runEffectFn2 step attrs as2 @@ -149,17 +171,27 @@ patchElem = EFn.mkEffectFn2 \state vdom → do pure $ mkStep $ Step node nextState patchElem haltElem _, _ → do let - onThese = EFn.mkEffectFn3 \ix s v → do - res ← EFn.runEffectFn2 step s v + -- both elements are found + onThese :: EFn.EffectFn3 Int (Step (VDom a w) DOM.Node) (VDom a w) (Step (VDom a w) DOM.Node) + onThese = EFn.mkEffectFn3 \ix (ch1Elem :: VDomStep a w) (ch2Elem :: VDom a w) → do + -- execute step function (compare previous dom and ch2Elem), the patchXXX function will be called for ch2Elem element + -- if elements are different - old element is removed from DOM, replaced with new but not yet attached to DOM + res ← EFn.runEffectFn2 step ch1Elem ch2Elem EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res - onThis = EFn.mkEffectFn2 \ix s → EFn.runEffectFn1 halt s - onThat = EFn.mkEffectFn2 \ix v → do - res ← EFn.runEffectFn1 build v + + -- there are no more new elements in the new list, but there is an element in old list + onThis :: EFn.EffectFn2 Int (Step (VDom a w) DOM.Node) Unit + onThis = EFn.mkEffectFn2 \ix ch1Elem → EFn.runEffectFn1 halt ch1Elem + + -- there are no more new elements in the old list, but there is an element in new list + onThat :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) + onThat = EFn.mkEffectFn2 \ix ch2Elem → do + res ← EFn.runEffectFn1 build ch2Elem EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res - children2 ← EFn.runEffectFn5 Util.diffWithIxE ch1 ch2 onThese onThis onThat - attrs2 ← EFn.runEffectFn2 step attrs as2 + (children2 :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn5 Util.diffWithIxE ch1 ch2 onThese onThis onThat + (attrs2 :: Step a Unit) ← EFn.runEffectFn2 step attrs as2 let nextState = { build @@ -195,13 +227,16 @@ buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let + node :: DOM.Node node = DOMElement.toNode el + + onChild :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn3 \k ix (Tuple _ vdom) → do res ← EFn.runEffectFn1 build vdom EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res - children ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 + (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild -- build keyed childrens + (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.buildAttributes el) as1 let state = { build @@ -237,11 +272,16 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do pure $ mkStep $ Step node nextState patchKeyed haltKeyed _, len2 → do let + onThese :: EFn.EffectFn4 String Int (Step (VDom a w) DOM.Node) (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) onThese = EFn.mkEffectFn4 \_ ix' s (Tuple _ v) → do res ← EFn.runEffectFn2 step s v EFn.runEffectFn3 Util.insertChildIx ix' (extract res) node pure res + + onThis :: EFn.EffectFn2 String (Step (VDom a w) DOM.Node) Unit onThis = EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s + + onThat :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) onThat = EFn.mkEffectFn3 \_ ix (Tuple _ v) → do res ← EFn.runEffectFn1 build v EFn.runEffectFn3 Util.insertChildIx ix (extract res) node @@ -279,6 +319,7 @@ buildWidget ∷ ∀ a w. VDomBuilder w a w buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w let + res' :: Step (VDom a w) DOM.Node res' = res # unStep \(Step n s k1 k2) → mkStep $ Step n { build, widget: res } patchWidget haltWidget pure res' diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 6c24315..950f115 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -13,7 +13,7 @@ import Prelude import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) -import Data.Nullable (null, toNullable) +import Data.Nullable (null, toNullable, Nullable) import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect) import Effect.Ref as Ref @@ -21,21 +21,50 @@ import Effect.Uncurried as EFn import Foreign (typeOf) import Foreign.Object as Object import Halogen.VDom as V -import Halogen.VDom.Machine (Step'(..), mkStep) +import Halogen.VDom.Machine (Step, Step'(..), mkStep) import Halogen.VDom.Types (Namespace(..)) import Halogen.VDom.Util as Util +import Halogen.VDom.Util (STObject') import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM -import Web.Event.EventTarget (eventListener) as DOM +import Web.Event.EventTarget (eventListener, EventListener) as DOM -- | Attributes, properties, event handlers, and element lifecycles. -- | Parameterized by the type of handlers outputs. + +-- | What is the difference between attributes and properties? +-- | +-- | Attributes are defined by HTML. Properties (on DOM elements) are defined by DOM. +-- | E.g. `class` attribute corresponds to `element.className` property +-- | almost always you should use properties on html elements, the svg elements don't have properties, only classes +-- | more https://github.com/purescript-halogen/purescript-halogen-vdom/issues/30#issuecomment-518015764 +-- | +-- | Also, attributes can be only strings, props - strings, numbers, booleans data Prop a - = Attribute (Maybe Namespace) String String - | Property String PropValue - | Handler DOM.EventType (DOM.Event → Maybe a) - | Ref (ElemRef DOM.Element → Maybe a) + = Attribute + -- XML namespace + (Maybe Namespace) + -- Attribute name + String + -- Attribute value + String + | Property + -- Property name. Usually is equal to attribute name, exeptions are: "htmlFor" property is a "for" attribute, "className" - "class" + String + PropValue + | Handler + -- Event type to listen to + DOM.EventType + -- Function that builds input for emitter (EmitterInputBuilder), if Nothing is returned - emitter is not called + -- NOTE: If multiple event handlers are added for the same event for the same element - only last event handler is going to work + -- (e.g. like in `H.div [HP.eventHandler (...), HP.eventHandler (...)]`) + (DOM.Event → Maybe a) + | Ref + -- This function builds input for emitter function too, but when parent element is removed or created + -- If Nothing is returned - emitter is not called + -- NOTE: If multiple ref handlers are added for the same element - only last ref handler is going to work + (ElemRef DOM.Element → Maybe a) instance functorProp ∷ Functor Prop where map f (Handler ty g) = Handler ty (map f <$> g) @@ -64,26 +93,50 @@ propFromInt = unsafeCoerce propFromNumber ∷ Number → PropValue propFromNumber = unsafeCoerce +type EmitterInputBuilder a = DOM.Event -> Maybe a +type EventListenerAndCurrentEmitterInputBuilder a = Tuple DOM.EventListener (Ref.Ref (EmitterInputBuilder a)) + +type PropState a = + { events :: Object.Object (EventListenerAndCurrentEmitterInputBuilder a) + , props :: Object.Object (Prop a) + } + -- | A `Machine`` for applying attributes, properties, and event handlers. -- | An emitter effect must be provided to respond to events. For example, -- | to allow arbitrary effects in event handlers, one could use `id`. buildProp ∷ ∀ a - . (a → Effect Unit) + . (a → Effect Unit) -- emitter, for example the global broadcaster function for all elements in halogen component → DOM.Element - → V.Machine (Array (Prop a)) Unit + → V.Machine (Array (Prop a)) Unit -- Machine takes array of properties for that element, outputs nothing buildProp emit el = renderProp where + -- what it does - creates a machine, that contains state + -- on next step - patches prop + -- on halt - all ref watchers are notified that element is removed + + renderProp :: EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) renderProp = EFn.mkEffectFn1 \ps1 → do - events ← Util.newMutMap - ps1' ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (applyProp events) + (events :: STObject' (EventListenerAndCurrentEmitterInputBuilder a)) ← Util.newMutMap + + -- for each prop in array: + -- if prop is attr - set attr to element, store attr under "attr/XXX" key in a returned object + -- if prop is property - set property to element, store property under "prop/XXX" key in a returned object + -- if prop is handler for DOM.EventType - start listen and add listener to `events` mutable map, store handler under "handler/EVENTTYPE" in a returned object + -- if prop is ref updater - store `emitterInputBuilder` in under a `ref` key in a returned object, call `emitter` on creation of all props (now) and on halt of all props (later) + (props :: Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (applyProp events) let - state = + (state :: PropState a) = { events: Util.unsafeFreeze events - , props: ps1' + , props } pure $ mkStep $ Step unit state patchProp haltProp + patchProp :: + EFn.EffectFn2 + (PropState a) + (Array (Prop a)) + (Step (Array (Prop a)) Unit) patchProp = EFn.mkEffectFn2 \state ps2 → do events ← Util.newMutMap let @@ -99,16 +152,24 @@ buildProp emit el = renderProp } pure $ mkStep $ Step unit nextState patchProp haltProp + haltProp + :: EFn.EffectFn1 + (PropState a) + Unit haltProp = EFn.mkEffectFn1 \state → do case Object.lookup "ref" state.props of - Just (Ref f) → - EFn.runEffectFn1 mbEmit (f (Removed el)) + Just (Ref emitterInputBuilder) → + EFn.runEffectFn1 mbEmit (emitterInputBuilder (Removed el)) _ → pure unit + mbEmit :: EFn.EffectFn1 (Maybe a) Unit mbEmit = EFn.mkEffectFn1 case _ of Just a → emit a _ → pure unit + applyProp + :: STObject' (EventListenerAndCurrentEmitterInputBuilder a) + -> EFn.EffectFn3 String Int (Prop a) (Prop a) applyProp events = EFn.mkEffectFn3 \_ _ v → case v of Attribute ns attr val → do @@ -117,23 +178,34 @@ buildProp emit el = renderProp Property prop val → do EFn.runEffectFn3 setProperty prop val el pure v - Handler (DOM.EventType ty) f → do - case Fn.runFn2 Util.unsafeGetAny ty events of - handler | Fn.runFn2 Util.unsafeHasAny ty events → do - Ref.write f (snd handler) + Handler (DOM.EventType eventType) emitterInputBuilder → do + case Fn.runFn2 Util.unsafeGetAny eventType events of + -- if eventType is already present in events storage / listened + handler | Fn.runFn2 Util.unsafeHasAny eventType events → do + -- replace current event listener with new + Ref.write emitterInputBuilder (snd handler) pure v _ → do - ref ← Ref.new f + ref ← Ref.new emitterInputBuilder listener ← DOM.eventListener \ev → do - f' ← Ref.read ref - EFn.runEffectFn1 mbEmit (f' ev) - EFn.runEffectFn3 Util.pokeMutMap ty (Tuple listener ref) events - EFn.runEffectFn3 Util.addEventListener ty listener el + (emitterInputBuilder' :: EmitterInputBuilder a) ← Ref.read ref + EFn.runEffectFn1 mbEmit (emitterInputBuilder' ev) + + -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder + EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events + + -- listen events of that type on the element + EFn.runEffectFn3 Util.addEventListener eventType listener el pure v - Ref f → do - EFn.runEffectFn1 mbEmit (f (Created el)) + Ref emitterInputBuilder → do + EFn.runEffectFn1 mbEmit (emitterInputBuilder (Created el)) pure v + diffProp + :: Fn.Fn2 + (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn4 String Int (Prop a) (Prop a) (Prop a)) diffProp = Fn.mkFn2 \prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → case v1, v2 of Attribute _ _ val1, Attribute ns2 attr2 val2 → @@ -156,15 +228,16 @@ buildProp emit el = renderProp _, _ → do EFn.runEffectFn3 setProperty prop2 val2 el pure v2 - Handler _ _, Handler (DOM.EventType ty) f → do + Handler _ _, Handler (DOM.EventType ty) emitterInputBuilder → do let handler = Fn.runFn2 Util.unsafeLookup ty prevEvents - Ref.write f (snd handler) + Ref.write emitterInputBuilder (snd handler) EFn.runEffectFn3 Util.pokeMutMap ty handler events pure v2 _, _ → pure v2 + removeProp :: Object.Object (EventListenerAndCurrentEmitterInputBuilder a) -> EFn.EffectFn2 String (Prop a) Unit removeProp prevEvents = EFn.mkEffectFn2 \_ v → case v of Attribute ns attr _ → @@ -194,10 +267,10 @@ unsafeGetProperty = Util.unsafeGetAny removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit removeProperty = EFn.mkEffectFn2 \key el → - EFn.runEffectFn3 Util.hasAttribute null key el >>= if _ - then EFn.runEffectFn3 Util.removeAttribute null key el + EFn.runEffectFn3 Util.hasAttribute (null :: Nullable Namespace) key el >>= if _ -- If attr exists on element + then EFn.runEffectFn3 Util.removeAttribute (null :: Nullable Namespace) key el -- remove it using el.removeAttribute() else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of - "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el + "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el -- If it's property - set it to "" _ → case key of "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el "colSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el diff --git a/src/Halogen/VDom/Machine.purs b/src/Halogen/VDom/Machine.purs index 072ff2a..469b18d 100644 --- a/src/Halogen/VDom/Machine.purs +++ b/src/Halogen/VDom/Machine.purs @@ -14,12 +14,31 @@ import Prelude import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2) import Unsafe.Coerce (unsafeCoerce) +{- + +type Machine is equal to: + +a -> Step a b +a -> forall state . Step b state (state -> a -> Step a b) (state -> Unit) +a -> forall state . Step b state (state -> Machine a b) (state -> Unit) + +where + +a is input +b is output +state is hidden state +(state -> a -> Step a b) is a functon from state and input to the new Step +(state -> Unit) is finalizer + +-} + type Machine a b = EffectFn1 a (Step a b) data Step' a b s = Step b s (EffectFn2 s a (Step a b)) (EffectFn1 s Unit) foreign import data Step ∷ Type → Type → Type +-- hides state type, makes it exsistential mkStep ∷ ∀ a b s. Step' a b s → Step a b mkStep = unsafeCoerce diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 3086916..013a0f6 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -24,7 +24,13 @@ foreign import data ThunkArg ∷ Type foreign import data ThunkId ∷ Type -data Thunk f i = Thunk ThunkId (Fn.Fn2 ThunkArg ThunkArg Boolean) (ThunkArg → f i) ThunkArg +--- widget type can be a thunk +data Thunk f i + = Thunk + ThunkId + (Fn.Fn2 ThunkArg ThunkArg Boolean) -- (oldArg -> newArg -> isEqual) + (ThunkArg → f i) -- (oldArg -> output) + ThunkArg -- oldArg unsafeThunkId ∷ ∀ a. a → ThunkId unsafeThunkId = unsafeCoerce @@ -81,13 +87,13 @@ runThunk ∷ ∀ f i. Thunk f i → f i runThunk (Thunk _ _ render arg) = render arg unsafeEqThunk ∷ ∀ f i. Fn.Fn2 (Thunk f i) (Thunk f i) Boolean -unsafeEqThunk = Fn.mkFn2 \(Thunk a1 b1 _ d1) (Thunk a2 b2 _ d2) → - Fn.runFn2 Util.refEq a1 a2 && - Fn.runFn2 Util.refEq b1 b2 && - Fn.runFn2 b1 d1 d2 +unsafeEqThunk = Fn.mkFn2 \(Thunk id eqFn _ renderArg) (Thunk id' eqFn' _ renderArg') → + Fn.runFn2 Util.refEq id id' && + Fn.runFn2 Util.refEq eqFn eqFn' && + Fn.runFn2 eqFn renderArg renderArg' type ThunkState f i a w = - { thunk ∷ Thunk f i + { thunk ∷ Thunk f i -- prev thunk , vdom ∷ M.Step (V.VDom a w) Node } @@ -106,10 +112,10 @@ buildThunk toVDom = renderThunk patchThunk ∷ EFn.EffectFn2 (ThunkState f i a w) (Thunk f i) (V.Step (Thunk f i) Node) patchThunk = EFn.mkEffectFn2 \state t2 → do let { vdom: prev, thunk: t1 } = state - if Fn.runFn2 unsafeEqThunk t1 t2 - then pure $ M.mkStep $ M.Step (M.extract prev) state patchThunk haltThunk + if Fn.runFn2 unsafeEqThunk t1 t2 -- if eq + then pure $ M.mkStep $ M.Step (M.extract prev) state patchThunk haltThunk -- dont run effect else do - vdom ← EFn.runEffectFn2 M.step prev (toVDom (runThunk t2)) + vdom ← EFn.runEffectFn2 M.step prev (toVDom (runThunk t2)) -- else create new vdom, execute step (compare and patch if need) pure $ M.mkStep $ M.Step (M.extract vdom) { vdom, thunk: t2 } patchThunk haltThunk haltThunk ∷ EFn.EffectFn1 (ThunkState f i a w) Unit diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index bf006bd..6583891 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -44,36 +44,26 @@ exports.replicateE = function (n, f) { } }; -exports.diffWithIxE = function (a1, a2, f1, f2, f3) { - var a3 = []; - var l1 = a1.length; - var l2 = a2.length; +exports.diffWithIxE = function (oldElems, newElems, onBothElements, onOldElement, onNewElement) { + var outputs = []; + var oldElemsLength = oldElems.length; + var newElemsLength = newElems.length; var i = 0; while (1) { - if (i < l1) { - if (i < l2) { - a3.push(f1(i, a1[i], a2[i])); + if (i < oldElemsLength) { + if (i < newElemsLength) { + outputs.push(onBothElements(i, oldElems[i], newElems[i])); } else { - f2(i, a1[i]); + onOldElement(i, oldElems[i]); } - } else if (i < l2) { - a3.push(f3(i, a2[i])); + } else if (i < newElemsLength) { + outputs.push(onNewElement(i, newElems[i])); } else { break; } i++; } - return a3; -}; - -exports.strMapWithIxE = function (as, fk, f) { - var o = {}; - for (var i = 0; i < as.length; i++) { - var a = as[i]; - var k = fk(a); - o[k] = f(k, i, a); - } - return o; + return outputs; }; exports.diffWithKeyAndIxE = function (o1, as, fk, f1, f2, f3) { @@ -96,6 +86,16 @@ exports.diffWithKeyAndIxE = function (o1, as, fk, f1, f2, f3) { return o2; }; +exports.strMapWithIxE = function (children, propToStrKey, f) { + var o = {}; + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var key = propToStrKey(child); + o[key] = f(key, i, child); + } + return o; +}; + exports.refEq = function (a, b) { return a === b; }; @@ -116,21 +116,22 @@ exports.createElement = function (ns, name, doc) { } }; -exports.insertChildIx = function (i, a, b) { - var n = b.childNodes.item(i) || null; - if (n !== a) { - b.insertBefore(a, n); +exports.insertChildIx = function (i, elem, parent) { + var referenceNode = parent.childNodes.item(i) || null; + if (referenceNode !== elem) { + // insert before referenceNode, if referenceNode is null - inserted at the end + parent.insertBefore(elem, referenceNode); } }; -exports.removeChild = function (a, b) { - if (b && a.parentNode === b) { - b.removeChild(a); +exports.removeChild = function (elem, parent) { + if (parent && elem.parentNode === parent) { + parent.removeChild(elem); } }; -exports.parentNode = function (a) { - return a.parentNode; +exports.parentNode = function (elem) { + return elem.parentNode; }; exports.setAttribute = function (ns, attr, val, el) { diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index cd4fb92..7fc48f7 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -29,6 +29,7 @@ module Halogen.VDom.Util , removeEventListener , JsUndefined , jsUndefined + , STObject' ) where import Prelude @@ -48,16 +49,18 @@ import Web.DOM.Element (Element) as DOM import Web.DOM.Node (Node) as DOM import Web.Event.EventTarget (EventListener) as DOM -newMutMap ∷ ∀ r a. Effect (STObject r a) +data STObject' a -- just like STObject, but without region + +newMutMap ∷ ∀ a. Effect (STObject' a) newMutMap = unsafeCoerce STObject.new -pokeMutMap ∷ ∀ r a. EFn.EffectFn3 String a (STObject r a) Unit +pokeMutMap ∷ ∀ a. EFn.EffectFn3 String a (STObject' a) Unit pokeMutMap = unsafeSetAny -deleteMutMap ∷ ∀ r a. EFn.EffectFn2 String (STObject r a) Unit +deleteMutMap ∷ ∀ a. EFn.EffectFn2 String (STObject' a) Unit deleteMutMap = unsafeDeleteAny -unsafeFreeze ∷ ∀ r a. STObject r a → Object a +unsafeFreeze ∷ ∀ a. STObject' a → Object a unsafeFreeze = unsafeCoerce unsafeLookup ∷ ∀ a. Fn.Fn2 String (Object a) a @@ -103,33 +106,33 @@ foreign import replicateE Unit foreign import diffWithIxE - ∷ ∀ b c d + ∷ ∀ oldElem newElem output dismissed . EFn.EffectFn5 - (Array b) - (Array c) - (EFn.EffectFn3 Int b c d) - (EFn.EffectFn2 Int b Unit) - (EFn.EffectFn2 Int c d) - (Array d) + (Array oldElem) -- e.g. list of vdom elements + (Array newElem) -- e.g. list of vdom elements + (EFn.EffectFn3 Int oldElem newElem output) -- execute action when both elems are found in oldElems array and newElems array under the same index (usually used to remove old element from DOM and add new element to DOM) + (EFn.EffectFn2 Int oldElem dismissed) -- execute action when only oldElem is found, there are no elems left in `Array newElem` (happens when array of old elements is bigger than array of new elements) + (EFn.EffectFn2 Int newElem output) -- execute action when only newElem is found, there are no elems left in `Array oldElem` (happens when array of new elements is bigger than array of old elements) + (Array output) -- e.g. list of dom elements foreign import diffWithKeyAndIxE - ∷ ∀ a b c d + ∷ ∀ oldElem newElemWithKey output dismissed . EFn.EffectFn6 - (Object.Object a) - (Array b) - (b → String) - (EFn.EffectFn4 String Int a b c) - (EFn.EffectFn2 String a d) - (EFn.EffectFn3 String Int b c) - (Object.Object c) + (Object.Object oldElem) + (Array newElemWithKey) + (newElemWithKey → String) + (EFn.EffectFn4 String Int oldElem newElemWithKey output) + (EFn.EffectFn2 String oldElem dismissed) + (EFn.EffectFn3 String Int newElemWithKey output) + (Object.Object output) foreign import strMapWithIxE - ∷ ∀ a b + ∷ ∀ child outputVal . EFn.EffectFn3 - (Array a) - (a → String) - (EFn.EffectFn3 String Int a b) - (Object.Object b) + (Array child) -- children + (child → String) -- propToStrKey + (EFn.EffectFn3 String Int child outputVal) -- action, executed on each array element, (StrKey -> Index -> child -> outputVal) + (Object.Object outputVal) -- key is StrKey, val type is outputVal foreign import refEq ∷ ∀ a b. Fn.Fn2 a b Boolean @@ -143,6 +146,9 @@ foreign import setTextContent foreign import createElement ∷ EFn.EffectFn3 (Nullable Namespace) ElemName DOM.Document DOM.Element +-- Insert new child at index +-- (if there is already an element on that index, that old element is moved below). +-- If there are not enough elements - new child is moved at the end of the list. foreign import insertChildIx ∷ EFn.EffectFn3 Int DOM.Node DOM.Node Unit diff --git a/test/Main.purs b/test/Main.purs index 7cc8be5..ed65188 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,39 +2,24 @@ module Test.Main where import Prelude -import Data.Bifunctor (bimap) import Data.Foldable (for_, traverse_) -import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..), isNothing) -import Data.Newtype (class Newtype, un, wrap) +import Data.Newtype (un, wrap) import Data.Tuple (Tuple(..)) import Effect (Effect) import Effect.Ref as Ref import Effect.Timer as Timer import Effect.Uncurried as EFn import Halogen.VDom as V -import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp) -import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk) -import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document (Document) as DOM +import Halogen.VDom.DOM.Prop (Prop) +import Halogen.VDom.Thunk (Thunk) import Web.DOM.Element (toNode) as DOM import Web.DOM.Node (Node, appendChild) as DOM import Web.DOM.ParentNode (querySelector) as DOM import Web.HTML (window) as DOM import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM import Web.HTML.Window (document) as DOM - -infixr 1 prop as := - -prop ∷ ∀ a. String → String → Prop a -prop key val = Property key (propFromString val) - -newtype VDom a = VDom (V.VDom (Array (Prop a)) (Thunk VDom a)) - -instance functorHtml ∷ Functor VDom where - map f (VDom vdom) = VDom (bimap (map (map f)) (map f) vdom) - -derive instance newtypeVDom ∷ Newtype (VDom a) _ +import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=)) type State = Array Database @@ -58,18 +43,6 @@ type DBQuery = initialState ∷ State initialState = [] -elem ∷ ∀ a. String → Array (Prop a) → Array (VDom a) → VDom a -elem n a c = VDom $ V.Elem Nothing (V.ElemName n) a (unsafeCoerce c) - -keyed ∷ ∀ a. String → Array (Prop a) → Array (Tuple String (VDom a)) → VDom a -keyed n a c = VDom $ V.Keyed Nothing (V.ElemName n) a (unsafeCoerce c) - -text ∷ ∀ a. String → VDom a -text a = VDom $ V.Text a - -thunk ∷ ∀ a b. (a → VDom b) → a → VDom b -thunk render val = VDom $ V.Widget $ Fn.runFn2 thunk1 render val - renderData ∷ State → VDom Void renderData st = elem "div" [] @@ -111,15 +84,6 @@ renderData st = ] ] -mkSpec - ∷ DOM.Document - → V.VDomSpec (Array (Prop Void)) (Thunk VDom Void) -mkSpec document = V.VDomSpec - { buildWidget: buildThunk (un VDom) - , buildAttributes: buildProp (const (pure unit)) - , document - } - foreign import getData ∷ Effect State foreign import getTimeout ∷ Effect Int diff --git a/test/TestVdom.purs b/test/TestVdom.purs new file mode 100644 index 0000000..1827802 --- /dev/null +++ b/test/TestVdom.purs @@ -0,0 +1,47 @@ +module Test.TestVdom where + +import Prelude + +import Data.Bifunctor (bimap) +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype, un) +import Data.Tuple (Tuple) +import Halogen.VDom as V +import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp) +import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk) +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Document (Document) as DOM + +infixr 1 prop as := + +prop ∷ ∀ a. String → String → Prop a +prop key val = Property key (propFromString val) + +newtype VDom a = VDom (V.VDom (Array (Prop a)) (Thunk VDom a)) + +instance functorHtml ∷ Functor VDom where + map f (VDom vdom) = VDom (bimap (map (map f)) (map f) vdom) + +derive instance newtypeVDom ∷ Newtype (VDom a) _ + +elem ∷ ∀ a. String → Array (Prop a) → Array (VDom a) → VDom a +elem n a c = VDom $ V.Elem Nothing (V.ElemName n) a (unsafeCoerce c) + +keyed ∷ ∀ a. String → Array (Prop a) → Array (Tuple String (VDom a)) → VDom a +keyed n a c = VDom $ V.Keyed Nothing (V.ElemName n) a (unsafeCoerce c) + +text ∷ ∀ a. String → VDom a +text a = VDom $ V.Text a + +thunk ∷ ∀ a b. (a → VDom b) → a → VDom b +thunk render val = VDom $ V.Widget $ Fn.runFn2 thunk1 render val + +mkSpec + ∷ DOM.Document + → V.VDomSpec (Array (Prop Void)) (Thunk VDom Void) +mkSpec document = V.VDomSpec + { buildWidget: buildThunk (un VDom) + , buildAttributes: buildProp (const (pure unit)) + , document + } From 9090846b75ce0fba0966f76c0d43429011eea7da Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 13 May 2020 20:09:59 +0300 Subject: [PATCH 02/48] feat: hydration -> WIP --- .gitignore | 2 ++ hydration-test.html | 28 +++++++++++++++++ package.json | 5 +-- test/Hydration.purs | 76 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 hydration-test.html create mode 100644 test/Hydration.purs diff --git a/.gitignore b/.gitignore index 577bc1e..b8140ce 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ package-lock.json /node_modules/ /output/ /app.js +/hydration-test.js +/yarn-error.log diff --git a/hydration-test.html b/hydration-test.html new file mode 100644 index 0000000..0ee7f9d --- /dev/null +++ b/hydration-test.html @@ -0,0 +1,28 @@ + + + + + + + hydration test + + + + + +
+
+
test label 1
+
test label 2
+
+
+ + + + + + diff --git a/package.json b/package.json index 7737036..670f8f1 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,9 @@ "scripts": { "clean": "rimraf output && rimraf", "test": "pulp build -I test -- --censor-lib --strict", - "build": "pulp build -- --censor-lib --strict" - "spago:test": "spago bundle-app --main Test.Main --to app.js" + "build": "pulp build -- --censor-lib --strict", + "spago:test": "spago bundle-app --main Test.Main --to app.js", + "spago:hydration-test": "spago bundle-app --main Test.Hydration --to hydration-test.js" }, "devDependencies": { "pulp": "^12.3.1", diff --git a/test/Hydration.purs b/test/Hydration.purs new file mode 100644 index 0000000..a86f889 --- /dev/null +++ b/test/Hydration.purs @@ -0,0 +1,76 @@ +module Test.Hydration where + +import Prelude + +import Control.Alternative (void) +import Data.Foldable (for_, traverse_) +import Data.Maybe (Maybe(..), isNothing, maybe) +import Data.Newtype (un, wrap) +import Data.Tuple (Tuple(..)) +import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Ref as Ref +import Effect.Timer as Timer +import Effect.Uncurried as EFn +import Halogen.VDom as V +import Halogen.VDom.DOM.Prop (Prop) +import Halogen.VDom.Thunk (Thunk) +import Halogen.VDom.Util (addEventListener) as Util +import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=)) +import Web.DOM.Element (Element) +import Web.DOM.Element (toNode) as DOM +import Web.DOM.Node (Node, appendChild) as DOM +import Web.DOM.ParentNode (ParentNode) +import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM +import Web.HTML (window) as DOM +import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM +import Web.HTML.Window (document) as DOM +import Web.Event.EventTarget (eventListener, EventListener) as DOM + +type State = Array { classes :: String, text :: String } + +initialState ∷ State +initialState = + [ { classes: "label1", text: "test label 1" } + , { classes: "label2", text: "test label 2" } + ] + +state2 ∷ State +state2 = + [ { classes: "label2", text: "test label 1.1" } + , { classes: "label1", text: "test label 2.1" } + ] + +renderData ∷ State → VDom Void +renderData st = + elem "div" [ "className" := "component" ] (st <#> renderElement) + where + renderElement elementState = + elem "div" + [ "className" := elementState.classes ] + [ text elementState.text ] + +findRequiredElement :: String -> ParentNode -> Effect Element +findRequiredElement selector parentNode = + DOM.querySelector (DOM.QuerySelector selector) parentNode + >>= maybe (throwException (error $ selector <> " not found")) pure + +main ∷ Effect Unit +main = do + win ← DOM.window + doc ← DOM.document win + appDiv ← findRequiredElement "#app" (DOM.toParentNode doc) + updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc) + + let + spec = mkSpec (DOM.toDocument doc) + initialValue = initialState + appDivNode = DOM.toNode appDiv + render = renderData + machine ← EFn.runEffectFn1 (V.buildVDom spec) (un VDom (render initialValue)) + void $ DOM.appendChild (V.extract machine) appDivNode + + listener ← DOM.eventListener \_ev → + void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) + + EFn.runEffectFn3 Util.addEventListener "click" listener updateStateButton From e29ca5cc0144d1a7530fc57f75ed4bce61108799 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 13 May 2020 22:17:02 +0300 Subject: [PATCH 03/48] feat: hydration -> WIP --- README.md | 3 + package.json | 4 +- src/Halogen/VDom.purs | 2 +- src/Halogen/VDom/DOM.purs | 342 +++---------------------------- src/Halogen/VDom/DOM/Elem.purs | 160 +++++++++++++++ src/Halogen/VDom/DOM/Keyed.purs | 118 +++++++++++ src/Halogen/VDom/DOM/Text.purs | 66 ++++++ src/Halogen/VDom/DOM/Types.purs | 59 ++++++ src/Halogen/VDom/DOM/Utils.purs | 80 ++++++++ src/Halogen/VDom/DOM/Widget.purs | 54 +++++ src/Halogen/VDom/Types.purs | 3 +- src/Halogen/VDom/Util.js | 20 ++ src/Halogen/VDom/Util.purs | 78 ++++--- test/Hydration.purs | 4 +- 14 files changed, 639 insertions(+), 354 deletions(-) create mode 100644 src/Halogen/VDom/DOM/Elem.purs create mode 100644 src/Halogen/VDom/DOM/Keyed.purs create mode 100644 src/Halogen/VDom/DOM/Text.purs create mode 100644 src/Halogen/VDom/DOM/Types.purs create mode 100644 src/Halogen/VDom/DOM/Utils.purs create mode 100644 src/Halogen/VDom/DOM/Widget.purs diff --git a/README.md b/README.md index 4694090..76e51a8 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,6 @@ implementation included). It is intended to be extended (and likely * Read the [guide](./GUIDE.md). * See the [test example](./test/Main.purs). + +TODO: +- use https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.server.js#L196-L201 diff --git a/package.json b/package.json index 670f8f1..4256eb4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "test": "pulp build -I test -- --censor-lib --strict", "build": "pulp build -- --censor-lib --strict", "spago:test": "spago bundle-app --main Test.Main --to app.js", - "spago:hydration-test": "spago bundle-app --main Test.Hydration --to hydration-test.js" + "spago:hydration-test": "spago bundle-app --main Test.Hydration --to hydration-test.js", + "spago:test:watch": "yarn run spago:test --watch", + "spago:hydration-test:watch": "yarn run spago:hydration-test --watch" }, "devDependencies": { "pulp": "^12.3.1", diff --git a/src/Halogen/VDom.purs b/src/Halogen/VDom.purs index 797aa13..18691f8 100644 --- a/src/Halogen/VDom.purs +++ b/src/Halogen/VDom.purs @@ -4,6 +4,6 @@ module Halogen.VDom , module Types ) where -import Halogen.VDom.DOM (VDomSpec(..), buildVDom) as DOM +import Halogen.VDom.DOM (VDomSpec(..), buildVDom, hydrateVDom) as DOM import Halogen.VDom.Machine (Machine, Step, Step'(..), mkStep, unStep, extract, step, halt) as Machine import Halogen.VDom.Types (VDom(..), Graft, runGraft, ElemName(..), Namespace(..)) as Types diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 7eda910..836df01 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -1,12 +1,14 @@ module Halogen.VDom.DOM - ( VDomSpec(..) + ( module Export , buildVDom - , buildText - , buildElem - , buildKeyed - , buildWidget + , hydrateVDom ) where +import Halogen.VDom.DOM.Elem +import Halogen.VDom.DOM.Keyed +import Halogen.VDom.DOM.Text +import Halogen.VDom.DOM.Types +import Halogen.VDom.DOM.Widget import Prelude import Data.Array as Array @@ -16,6 +18,12 @@ import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) import Effect.Uncurried as EFn import Foreign.Object as Object +import Halogen.VDom.DOM.Elem (buildElem) as Export +import Halogen.VDom.DOM.Keyed (buildKeyed) as Export +import Halogen.VDom.DOM.Text (buildText) as Export +import Halogen.VDom.DOM.Types (VDomSpec(..)) as Export +import Halogen.VDom.DOM.Utils (undefined) +import Halogen.VDom.DOM.Widget (buildWidget) as Export import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) @@ -25,35 +33,6 @@ import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOMElement import Web.DOM.Node (Node) as DOM --- A function, that takes `VDom a w` and builds a `DOM.Node` -type VDomMachine a w = Machine (VDom a w) DOM.Node - -type VDomStep a w = Step (VDom a w) DOM.Node - -type VDomInit i a w = EFn.EffectFn1 i (VDomStep a w) - --- Equal to --- (VDomSpec a w) -> (VDOM a w -> Step (VDOM a w) DOM.Node) -> i -> Effect (Step (VDOM a w) DOM.Node) -type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomStep a w) - -type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i j k l (VDomStep a w) - --- | Widget machines recursively reference the configured spec to potentially --- | enable recursive trees of Widgets. -newtype VDomSpec a w = VDomSpec - { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` - -- example: - - -- buildAttributes = buildProps handler - -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/VDom/Driver.purs#L68-L71 - - -- what is handler - -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 - , buildAttributes ∷ DOM.Element → Machine a Unit - -- We need document to be able to call `document.createElement` function - , document ∷ DOM.Document - } - -- | Starts an initial `VDom` machine by providing a `VDomSpec`. -- | -- | ```purescript @@ -63,292 +42,23 @@ newtype VDomSpec a w = VDomSpec -- | machine3 ← Machine.step machine2 vdomTree3 -- | ... -- | ```` +hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Node -> VDomMachine a w +hydrateVDom spec rootNode = hydrate + where + build = buildVDom spec + hydrate = EFn.mkEffectFn1 case _ of + Text s → EFn.runEffectFn5 hydrateText rootNode spec hydrate build s + Elem namespace elemName attribute childrenVdoms → EFn.runEffectFn8 hydrateElem rootNode spec hydrate build namespace elemName attribute childrenVdoms + Keyed namespace elemName attribute keyedChildrenVdoms → undefined + Widget w → undefined + Grafted g → undefined + buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where build = EFn.mkEffectFn1 case _ of Text s → EFn.runEffectFn3 buildText spec build s -- build text machine - Elem ns n a ch → EFn.runEffectFn6 buildElem spec build ns n a ch - Keyed ns n a keyedCh → EFn.runEffectFn6 buildKeyed spec build ns n a keyedCh + Elem namespace elemName a childrenVdoms → EFn.runEffectFn6 buildElem spec build namespace elemName a childrenVdoms + Keyed namespace elemName a keyedChildrenVdoms → EFn.runEffectFn6 buildKeyed spec build namespace elemName a keyedChildrenVdoms Widget w → EFn.runEffectFn3 buildWidget spec build w -- machine that has full control of it's lifecycle Grafted g → EFn.runEffectFn1 build (runGraft g) -- optimization - -type TextState a w = - { build ∷ VDomMachine a w - , node ∷ DOM.Node - , value ∷ String - } - -buildText ∷ ∀ a w. VDomBuilder String a w -buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do - node ← EFn.runEffectFn2 Util.createTextNode s spec.document - let (state :: TextState a w) = { build, node, value: s } - pure $ mkStep $ Step node state patchText haltText - -patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) -patchText = EFn.mkEffectFn2 \state newVdom → do - let { build, node, value: value1 } = state - case newVdom of - Grafted g → - EFn.runEffectFn2 patchText state (runGraft g) -- Before there was a Text on this place. We call patchText instead of patch to be able to remove text - Text value2 - | value1 == value2 → - pure $ mkStep $ Step node state patchText haltText - | otherwise → do - let nextState = { build, node, value: value2 } - EFn.runEffectFn2 Util.setTextContent value2 node - pure $ mkStep $ Step node nextState patchText haltText - _ → do - EFn.runEffectFn1 haltText state - EFn.runEffectFn1 build newVdom - -haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit -haltText = EFn.mkEffectFn1 \{ node } → do - parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent - -type ElemState a w = - { build ∷ VDomMachine a w - , node ∷ DOM.Node - , attrs ∷ Step a Unit - , ns ∷ Maybe Namespace - , name ∷ ElemName - , children ∷ Array (VDomStep a w) - } - -buildElem - ∷ ∀ a w - . VDomBuilder4 - (Maybe Namespace) - ElemName - a - (Array (VDom a w)) - a - w -buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do - el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document - let - node :: DOM.Node - node = DOMElement.toNode el - - onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn2 \ix child → do - (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 build child - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node - pure res - children ← EFn.runEffectFn2 Util.forE ch1 onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 -- build machine that takes attributes - let - state = - { build - , node - , attrs - , ns: ns1 - , name: name1 - , children - } - pure $ mkStep $ Step node state patchElem haltElem - -patchElem ∷ ∀ a w. EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) -patchElem = EFn.mkEffectFn2 \state vdom → do - let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state - case vdom of - Grafted g → - EFn.runEffectFn2 patchElem state (runGraft g) - Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do -- if new vdom is elem AND new and old are equal - case Array.length ch1, Array.length ch2 of - 0, 0 → do - attrs2 ← EFn.runEffectFn2 step attrs as2 - let - nextState = - { build - , node - , attrs: attrs2 - , ns: ns2 - , name: name2 - , children: ch1 - } - pure $ mkStep $ Step node nextState patchElem haltElem - _, _ → do - let - -- both elements are found - onThese :: EFn.EffectFn3 Int (Step (VDom a w) DOM.Node) (VDom a w) (Step (VDom a w) DOM.Node) - onThese = EFn.mkEffectFn3 \ix (ch1Elem :: VDomStep a w) (ch2Elem :: VDom a w) → do - -- execute step function (compare previous dom and ch2Elem), the patchXXX function will be called for ch2Elem element - -- if elements are different - old element is removed from DOM, replaced with new but not yet attached to DOM - res ← EFn.runEffectFn2 step ch1Elem ch2Elem - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node - pure res - - -- there are no more new elements in the new list, but there is an element in old list - onThis :: EFn.EffectFn2 Int (Step (VDom a w) DOM.Node) Unit - onThis = EFn.mkEffectFn2 \ix ch1Elem → EFn.runEffectFn1 halt ch1Elem - - -- there are no more new elements in the old list, but there is an element in new list - onThat :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) - onThat = EFn.mkEffectFn2 \ix ch2Elem → do - res ← EFn.runEffectFn1 build ch2Elem - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node - pure res - (children2 :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn5 Util.diffWithIxE ch1 ch2 onThese onThis onThat - (attrs2 :: Step a Unit) ← EFn.runEffectFn2 step attrs as2 - let - nextState = - { build - , node - , attrs: attrs2 - , ns: ns2 - , name: name2 - , children: children2 - } - pure $ mkStep $ Step node nextState patchElem haltElem - _ → do - EFn.runEffectFn1 haltElem state - EFn.runEffectFn1 build vdom - -haltElem ∷ ∀ a w. EFn.EffectFn1 (ElemState a w) Unit -haltElem = EFn.mkEffectFn1 \{ node, attrs, children } → do - parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent - EFn.runEffectFn2 Util.forEachE children halt - EFn.runEffectFn1 halt attrs - -type KeyedState a w = - { build ∷ VDomMachine a w - , node ∷ DOM.Node - , attrs ∷ Step a Unit - , ns ∷ Maybe Namespace - , name ∷ ElemName - , children ∷ Object.Object (VDomStep a w) - , length ∷ Int - } - -buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w -buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do - el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document - let - node :: DOM.Node - node = DOMElement.toNode el - - onChild :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn3 \k ix (Tuple _ vdom) → do - res ← EFn.runEffectFn1 build vdom - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node - pure res - (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild -- build keyed childrens - (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.buildAttributes el) as1 - let - state = - { build - , node - , attrs - , ns: ns1 - , name: name1 - , children - , length: Array.length ch1 - } - pure $ mkStep $ Step node state patchKeyed haltKeyed - -patchKeyed ∷ ∀ a w. EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) -patchKeyed = EFn.mkEffectFn2 \state vdom → do - let { build, node, attrs, ns: ns1, name: name1, children: ch1, length: len1 } = state - case vdom of - Grafted g → - EFn.runEffectFn2 patchKeyed state (runGraft g) - Keyed ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → - case len1, Array.length ch2 of - 0, 0 → do - attrs2 ← EFn.runEffectFn2 Machine.step attrs as2 - let - nextState = - { build - , node - , attrs: attrs2 - , ns: ns2 - , name: name2 - , children: ch1 - , length: 0 - } - pure $ mkStep $ Step node nextState patchKeyed haltKeyed - _, len2 → do - let - onThese :: EFn.EffectFn4 String Int (Step (VDom a w) DOM.Node) (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) - onThese = EFn.mkEffectFn4 \_ ix' s (Tuple _ v) → do - res ← EFn.runEffectFn2 step s v - EFn.runEffectFn3 Util.insertChildIx ix' (extract res) node - pure res - - onThis :: EFn.EffectFn2 String (Step (VDom a w) DOM.Node) Unit - onThis = EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s - - onThat :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) - onThat = EFn.mkEffectFn3 \_ ix (Tuple _ v) → do - res ← EFn.runEffectFn1 build v - EFn.runEffectFn3 Util.insertChildIx ix (extract res) node - pure res - children2 ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ch1 ch2 fst onThese onThis onThat - attrs2 ← EFn.runEffectFn2 step attrs as2 - let - nextState = - { build - , node - , attrs: attrs2 - , ns: ns2 - , name: name2 - , children: children2 - , length: len2 - } - pure $ mkStep $ Step node nextState patchKeyed haltKeyed - _ → do - EFn.runEffectFn1 haltKeyed state - EFn.runEffectFn1 build vdom - -haltKeyed ∷ ∀ a w. EFn.EffectFn1 (KeyedState a w) Unit -haltKeyed = EFn.mkEffectFn1 \{ node, attrs, children } → do - parent ← EFn.runEffectFn1 Util.parentNode node - EFn.runEffectFn2 Util.removeChild node parent - EFn.runEffectFn2 Util.forInE children (EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s) - EFn.runEffectFn1 halt attrs - -type WidgetState a w = - { build ∷ VDomMachine a w - , widget ∷ Step w DOM.Node - } - -buildWidget ∷ ∀ a w. VDomBuilder w a w -buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do - res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w - let - res' :: Step (VDom a w) DOM.Node - res' = res # unStep \(Step n s k1 k2) → - mkStep $ Step n { build, widget: res } patchWidget haltWidget - pure res' - -patchWidget ∷ ∀ a w. EFn.EffectFn2 (WidgetState a w) (VDom a w) (VDomStep a w) -patchWidget = EFn.mkEffectFn2 \state vdom → do - let { build, widget } = state - case vdom of - Grafted g → - EFn.runEffectFn2 patchWidget state (runGraft g) - Widget w → do - res ← EFn.runEffectFn2 step widget w - let - res' = res # unStep \(Step n s k1 k2) → - mkStep $ Step n { build, widget: res } patchWidget haltWidget - pure res' - _ → do - EFn.runEffectFn1 haltWidget state - EFn.runEffectFn1 build vdom - -haltWidget ∷ forall a w. EFn.EffectFn1 (WidgetState a w) Unit -haltWidget = EFn.mkEffectFn1 \{ widget } → do - EFn.runEffectFn1 halt widget - -eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean -eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → - if name1 == name2 - then case ns1, ns2 of - Just (Namespace ns1'), Just (Namespace ns2') | ns1' == ns2' → true - Nothing, Nothing → true - _, _ → false - else false diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs new file mode 100644 index 0000000..d945bc4 --- /dev/null +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -0,0 +1,160 @@ +module Halogen.VDom.DOM.Elem where + +import Halogen.VDom.DOM.Types +import Halogen.VDom.DOM.Utils +import Prelude + +import Data.Array (length) +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM + +type ElemState a w = + { build ∷ VDomMachine a w + , node ∷ DOM.Node + , attrs ∷ Step a Unit + , ns ∷ Maybe Namespace + , name ∷ ElemName + , children ∷ Array (VDomStep a w) + } + +hydrateElem + ∷ ∀ a w + . VDomHydrator4 + (Maybe Namespace) + ElemName + a + (Array (VDom a w)) + a + w +hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do + checkNodeIsElementNode currentNode + checkNodeNamespaceIsEqualTo (toNullable ns1) currentNode + checkNodeNameIsEqualTo name1 currentNode + checkNodeChildrenLengthIsEqualTo (length ch1) currentNode + let + onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) + onChild = undefined + children ← undefined + attrs ← undefined + let + state = + { build + , node: currentNode + , attrs + , ns: ns1 + , name: name1 + , children + } + pure $ mkStep $ Step currentNode state patchElem haltElem + +buildElem + ∷ ∀ a w + . VDomBuilder4 + (Maybe Namespace) + ElemName + a + (Array (VDom a w)) + a + w +buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do + el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document + let + node :: DOM.Node + node = DOMElement.toNode el + + onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn2 \ix child → do + (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 build child + EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + pure res + children ← EFn.runEffectFn2 Util.forE ch1 onChild + attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 -- build machine that takes attributes + let + state = + { build + , node + , attrs + , ns: ns1 + , name: name1 + , children + } + pure $ mkStep $ Step node state patchElem haltElem + +patchElem ∷ ∀ a w. EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) +patchElem = EFn.mkEffectFn2 \state vdom → do + let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state + case vdom of + Grafted g → + EFn.runEffectFn2 patchElem state (runGraft g) + Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do -- if new vdom is elem AND new and old are equal + case Array.length ch1, Array.length ch2 of + 0, 0 → do + attrs2 ← EFn.runEffectFn2 step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: ch1 + } + pure $ mkStep $ Step node nextState patchElem haltElem + _, _ → do + let + -- both elements are found + onThese :: EFn.EffectFn3 Int (Step (VDom a w) DOM.Node) (VDom a w) (Step (VDom a w) DOM.Node) + onThese = EFn.mkEffectFn3 \ix (ch1Elem :: VDomStep a w) (ch2Elem :: VDom a w) → do + -- execute step function (compare previous dom and ch2Elem), the patchXXX function will be called for ch2Elem element + -- if elements are different - old element is removed from DOM, replaced with new but not yet attached to DOM + res ← EFn.runEffectFn2 step ch1Elem ch2Elem + EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + pure res + + -- there are no more new elements in the new list, but there is an element in old list + onThis :: EFn.EffectFn2 Int (Step (VDom a w) DOM.Node) Unit + onThis = EFn.mkEffectFn2 \ix ch1Elem → EFn.runEffectFn1 halt ch1Elem + + -- there are no more new elements in the old list, but there is an element in new list + onThat :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) + onThat = EFn.mkEffectFn2 \ix ch2Elem → do + res ← EFn.runEffectFn1 build ch2Elem + EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + pure res + (children2 :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn5 Util.diffWithIxE ch1 ch2 onThese onThis onThat + (attrs2 :: Step a Unit) ← EFn.runEffectFn2 step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: children2 + } + pure $ mkStep $ Step node nextState patchElem haltElem + _ → do + EFn.runEffectFn1 haltElem state + EFn.runEffectFn1 build vdom + +haltElem ∷ ∀ a w. EFn.EffectFn1 (ElemState a w) Unit +haltElem = EFn.mkEffectFn1 \{ node, attrs, children } → do + parent ← EFn.runEffectFn1 Util.parentNode node + EFn.runEffectFn2 Util.removeChild node parent + EFn.runEffectFn2 Util.forEachE children halt + EFn.runEffectFn1 halt attrs diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs new file mode 100644 index 0000000..efbb23c --- /dev/null +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -0,0 +1,118 @@ +module Halogen.VDom.DOM.Keyed where + +import Prelude + +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM +import Halogen.VDom.DOM.Types +import Halogen.VDom.DOM.Utils + +type KeyedState a w = + { build ∷ VDomMachine a w + , node ∷ DOM.Node + , attrs ∷ Step a Unit + , ns ∷ Maybe Namespace + , name ∷ ElemName + , children ∷ Object.Object (VDomStep a w) + , length ∷ Int + } + +buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w +buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do + el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document + let + node :: DOM.Node + node = DOMElement.toNode el + + onChild :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn3 \k ix (Tuple _ vdom) → do + res ← EFn.runEffectFn1 build vdom + EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + pure res + (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild -- build keyed childrens + (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.buildAttributes el) as1 + let + state = + { build + , node + , attrs + , ns: ns1 + , name: name1 + , children + , length: Array.length ch1 + } + pure $ mkStep $ Step node state patchKeyed haltKeyed + +patchKeyed ∷ ∀ a w. EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) +patchKeyed = EFn.mkEffectFn2 \state vdom → do + let { build, node, attrs, ns: ns1, name: name1, children: ch1, length: len1 } = state + case vdom of + Grafted g → + EFn.runEffectFn2 patchKeyed state (runGraft g) + Keyed ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → + case len1, Array.length ch2 of + 0, 0 → do + attrs2 ← EFn.runEffectFn2 Machine.step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: ch1 + , length: 0 + } + pure $ mkStep $ Step node nextState patchKeyed haltKeyed + _, len2 → do + let + onThese :: EFn.EffectFn4 String Int (Step (VDom a w) DOM.Node) (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) + onThese = EFn.mkEffectFn4 \_ ix' s (Tuple _ v) → do + res ← EFn.runEffectFn2 step s v + EFn.runEffectFn3 Util.insertChildIx ix' (extract res) node + pure res + + onThis :: EFn.EffectFn2 String (Step (VDom a w) DOM.Node) Unit + onThis = EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s + + onThat :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) + onThat = EFn.mkEffectFn3 \_ ix (Tuple _ v) → do + res ← EFn.runEffectFn1 build v + EFn.runEffectFn3 Util.insertChildIx ix (extract res) node + pure res + children2 ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ch1 ch2 fst onThese onThis onThat + attrs2 ← EFn.runEffectFn2 step attrs as2 + let + nextState = + { build + , node + , attrs: attrs2 + , ns: ns2 + , name: name2 + , children: children2 + , length: len2 + } + pure $ mkStep $ Step node nextState patchKeyed haltKeyed + _ → do + EFn.runEffectFn1 haltKeyed state + EFn.runEffectFn1 build vdom + +haltKeyed ∷ ∀ a w. EFn.EffectFn1 (KeyedState a w) Unit +haltKeyed = EFn.mkEffectFn1 \{ node, attrs, children } → do + parent ← EFn.runEffectFn1 Util.parentNode node + EFn.runEffectFn2 Util.removeChild node parent + EFn.runEffectFn2 Util.forInE children (EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s) + EFn.runEffectFn1 halt attrs diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs new file mode 100644 index 0000000..c9fc8ce --- /dev/null +++ b/src/Halogen/VDom/DOM/Text.purs @@ -0,0 +1,66 @@ +module Halogen.VDom.DOM.Text where + +import Halogen.VDom.DOM.Types +import Prelude + +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Halogen.VDom.DOM.Utils +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM +import Web.DOM.Node (textContent) + +type TextState a w = + { build ∷ VDomMachine a w + , node ∷ DOM.Node + , value ∷ String + } + +buildText ∷ ∀ a w. VDomBuilder String a w +buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do + node ← EFn.runEffectFn2 Util.createTextNode s spec.document + let (state :: TextState a w) = { build, node, value: s } + pure $ mkStep $ Step node state patchText haltText + +-- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? +hydrateText ∷ ∀ a w. VDomHydrator String a w +hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → do + checkNodeIsTextNode currentNode + checkNodeTextContentIsEqTo s currentNode + let (state :: TextState a w) = { build, node: currentNode, value: s } + pure $ mkStep $ Step currentNode state patchText haltText + +patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) +patchText = EFn.mkEffectFn2 \state newVdom → do + let { build, node, value: value1 } = state + case newVdom of + Grafted g → + EFn.runEffectFn2 patchText state (runGraft g) -- Before there was a Text on this place. We call patchText instead of patch to be able to remove text + Text value2 + | value1 == value2 → + pure $ mkStep $ Step node state patchText haltText + | otherwise → do + let nextState = { build, node, value: value2 } + EFn.runEffectFn2 Util.setTextContent value2 node + pure $ mkStep $ Step node nextState patchText haltText + _ → do + EFn.runEffectFn1 haltText state + EFn.runEffectFn1 build newVdom + +haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit +haltText = EFn.mkEffectFn1 \{ node } → do + parent ← EFn.runEffectFn1 Util.parentNode node + EFn.runEffectFn2 Util.removeChild node parent diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs new file mode 100644 index 0000000..c5d1b06 --- /dev/null +++ b/src/Halogen/VDom/DOM/Types.purs @@ -0,0 +1,59 @@ +module Halogen.VDom.DOM.Types where + +import Prelude + +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM + +-- A function, that takes `VDom a w` and builds a `DOM.Node` +type VDomMachine a w = Machine (VDom a w) DOM.Node + +type VDomStep a w = Step (VDom a w) DOM.Node + +type VDomInit i a w = EFn.EffectFn1 i (VDomStep a w) + +-- Equal to +-- (VDomSpec a w) -> (VDOM a w -> Step (VDOM a w) DOM.Node) -> i -> Effect (Step (VDOM a w) DOM.Node) +type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomStep a w) + +type VDomHydrator i a w + = EFn.EffectFn5 + DOM.Node + (VDomSpec a w) + (VDomMachine a w) -- top hydrate function + (VDomMachine a w) -- top build function + i + (VDomStep a w) + +type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i j k l (VDomStep a w) + +type VDomHydrator4 i j k l a w = EFn.EffectFn8 DOM.Node (VDomSpec a w) (VDomMachine a w) (VDomMachine a w) i j k l (VDomStep a w) + +-- | Widget machines recursively reference the configured spec to potentially +-- | enable recursive trees of Widgets. +newtype VDomSpec a w = VDomSpec + { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` + -- example: + + -- buildAttributes = buildProps handler + -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/VDom/Driver.purs#L68-L71 + + -- what is handler + -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 + , buildAttributes ∷ DOM.Element → Machine a Unit + -- We need document to be able to call `document.createElement` function + , document ∷ DOM.Document + } diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Utils.purs new file mode 100644 index 0000000..ff43168 --- /dev/null +++ b/src/Halogen/VDom/DOM/Utils.purs @@ -0,0 +1,80 @@ +module Halogen.VDom.DOM.Utils where + +import Halogen.VDom.DOM.Types +import Prelude + +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (Nullable, toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM + +eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean +eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → + if name1 == name2 + then case ns1, ns2 of + Just (Namespace ns1'), Just (Namespace ns2') | ns1' == ns2' → true + Nothing, Nothing → true + _, _ → false + else false + +quote :: String -> String +quote s = "\"" <> s <> "\"" + +-------------------------------------- +-- Text + +checkNodeIsTextNode :: DOM.Node -> Effect Unit +checkNodeIsTextNode node = + EFn.runEffectFn1 Util.nodeIsTextNode node >>= if _ + then pure unit + else do + nodeType <- EFn.runEffectFn1 Util.getNodeType node + throwException (error $ "Expected node to be a text node (nodeType === 3), but got " <> show nodeType) + +checkNodeTextContentIsEqTo :: String -> DOM.Node -> Effect Unit +checkNodeTextContentIsEqTo s node = do + textContent <- EFn.runEffectFn1 Util.getTextContent node + if textContent == s + then pure unit + else throwException (error $ "Expected node text content to equal to " <> quote s <> ", but got " <> quote textContent) + +-------------------------------------- +-- Elem + +checkNodeIsElementNode :: DOM.Node -> Effect Unit +checkNodeIsElementNode node = + EFn.runEffectFn1 Util.nodeIsElementNode node >>= if _ + then pure unit + else do + nodeType <- EFn.runEffectFn1 Util.getNodeType node + throwException (error $ "Expected node to be element node (nodeType === 1), but got " <> show nodeType) + +checkNodeNamespaceIsEqualTo :: Nullable Namespace -> DOM.Node -> Effect Unit +checkNodeNamespaceIsEqualTo namespace node = do + nullableNamespaceURI <- EFn.runEffectFn1 Util.getNamespaceURI node + if nullableNamespaceURI == namespace + then pure unit + else throwException (error $ "Expected node namespaceURI equal to " <> quote (show namespace) <> ", but got " <> quote (show nullableNamespaceURI)) + +checkNodeNameIsEqualTo :: ElemName -> DOM.Node -> Effect Unit +checkNodeNameIsEqualTo = undefined + +checkNodeChildrenLengthIsEqualTo :: Int -> DOM.Node -> Effect Unit +checkNodeChildrenLengthIsEqualTo = undefined + +undefined :: ∀ a . a +undefined = unsafeCoerce unit diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs new file mode 100644 index 0000000..a79b48f --- /dev/null +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -0,0 +1,54 @@ +module Halogen.VDom.DOM.Widget where + +import Prelude + +import Data.Array as Array +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toNullable) +import Data.Tuple (Tuple(..), fst) +import Effect.Uncurried as EFn +import Foreign.Object as Object +import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine as Machine +import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Util as Util +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOMElement +import Web.DOM.Node (Node) as DOM +import Halogen.VDom.DOM.Types + +type WidgetState a w = + { build ∷ VDomMachine a w + , widget ∷ Step w DOM.Node + } + +buildWidget ∷ ∀ a w. VDomBuilder w a w +buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do + res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w + let + res' :: Step (VDom a w) DOM.Node + res' = res # unStep \(Step n s k1 k2) → + mkStep $ Step n { build, widget: res } patchWidget haltWidget + pure res' + +patchWidget ∷ ∀ a w. EFn.EffectFn2 (WidgetState a w) (VDom a w) (VDomStep a w) +patchWidget = EFn.mkEffectFn2 \state vdom → do + let { build, widget } = state + case vdom of + Grafted g → + EFn.runEffectFn2 patchWidget state (runGraft g) + Widget w → do + res ← EFn.runEffectFn2 step widget w + let + res' = res # unStep \(Step n s k1 k2) → + mkStep $ Step n { build, widget: res } patchWidget haltWidget + pure res' + _ → do + EFn.runEffectFn1 haltWidget state + EFn.runEffectFn1 build vdom + +haltWidget ∷ forall a w. EFn.EffectFn1 (WidgetState a w) Unit +haltWidget = EFn.mkEffectFn1 \{ widget } → do + EFn.runEffectFn1 halt widget diff --git a/src/Halogen/VDom/Types.purs b/src/Halogen/VDom/Types.purs index 9500cb5..9692e22 100644 --- a/src/Halogen/VDom/Types.purs +++ b/src/Halogen/VDom/Types.purs @@ -25,7 +25,7 @@ import Unsafe.Coerce (unsafeCoerce) -- | fusion using a Coyoneda-like encoding. data VDom a w = Text String - | Elem (Maybe Namespace) ElemName a (Array (VDom a w)) + | Elem (Maybe Namespace) ElemName a (Array (VDom a w)) -- TODO: use list instead of array, as elm doint it https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.js#L1531 | Keyed (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) | Widget w | Grafted (Graft a w) @@ -102,3 +102,4 @@ newtype Namespace = Namespace String derive instance newtypeNamespace ∷ Newtype Namespace _ derive newtype instance eqNamespace ∷ Eq Namespace derive newtype instance ordNamespace ∷ Ord Namespace +derive newtype instance showNamespace ∷ Show Namespace diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index 6583891..116f489 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -167,3 +167,23 @@ exports.removeEventListener = function (ev, listener, el) { }; exports.jsUndefined = void 0; + +exports.getNodeType = function(el) { + return el.nodeType +} + +exports.nodeIsTextNode = function(el) { + return el.nodeType === 3 +} + +exports.nodeIsElementNode = function(el) { + return el.nodeType === 1 +} + +exports.getTextContent = function(el) { + return node.textContent; +} + +exports.getNamespaceURI = function(el) { + return node.namespaceURI +} diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 7fc48f7..03bf2e2 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -1,36 +1,38 @@ -module Halogen.VDom.Util - ( newMutMap - , pokeMutMap - , deleteMutMap - , unsafeFreeze - , unsafeLookup - , unsafeGetAny - , unsafeHasAny - , unsafeSetAny - , unsafeDeleteAny - , forE - , forEachE - , forInE - , replicateE - , diffWithIxE - , diffWithKeyAndIxE - , strMapWithIxE - , refEq - , createTextNode - , setTextContent - , createElement - , insertChildIx - , removeChild - , parentNode - , setAttribute - , removeAttribute - , hasAttribute - , addEventListener - , removeEventListener - , JsUndefined - , jsUndefined - , STObject' - ) where +module Halogen.VDom.Util where + -- | ( newMutMap + -- | , pokeMutMap + -- | , deleteMutMap + -- | , unsafeFreeze + -- | , unsafeLookup + -- | , unsafeGetAny + -- | , unsafeHasAny + -- | , unsafeSetAny + -- | , unsafeDeleteAny + -- | , forE + -- | , forEachE + -- | , forInE + -- | , replicateE + -- | , diffWithIxE + -- | , diffWithKeyAndIxE + -- | , strMapWithIxE + -- | , refEq + -- | , createTextNode + -- | , setTextContent + -- | , createElement + -- | , insertChildIx + -- | , removeChild + -- | , parentNode + -- | , setAttribute + -- | , removeAttribute + -- | , hasAttribute + -- | , addEventListener + -- | , removeEventListener + -- | , JsUndefined + -- | , jsUndefined + -- | , STObject' + -- | , nodeIsTextNode + -- | , nodeIsElementNode + -- | ) where import Prelude @@ -176,3 +178,13 @@ foreign import removeEventListener foreign import data JsUndefined ∷ Type foreign import jsUndefined ∷ JsUndefined + +foreign import getNodeType :: EFn.EffectFn1 DOM.Node Int + +foreign import nodeIsTextNode :: EFn.EffectFn1 DOM.Node Boolean + +foreign import nodeIsElementNode :: EFn.EffectFn1 DOM.Node Boolean + +foreign import getTextContent :: EFn.EffectFn1 DOM.Node String + +foreign import getNamespaceURI :: EFn.EffectFn1 DOM.Node (Nullable Namespace) diff --git a/test/Hydration.purs b/test/Hydration.purs index a86f889..c5e063a 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -67,8 +67,8 @@ main = do initialValue = initialState appDivNode = DOM.toNode appDiv render = renderData - machine ← EFn.runEffectFn1 (V.buildVDom spec) (un VDom (render initialValue)) - void $ DOM.appendChild (V.extract machine) appDivNode + initialVdom = un VDom (render initialValue) + machine ← EFn.runEffectFn1 (V.hydrateVDom spec appDivNode) initialVdom listener ← DOM.eventListener \_ev → void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) From fdbd77794713202f39e16bf6155daa347cbb8e52 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 18:45:38 +0300 Subject: [PATCH 04/48] refactor: use web-dom --- src/Halogen/VDom/DOM.purs | 2 +- src/Halogen/VDom/DOM/Elem.purs | 13 +++--- src/Halogen/VDom/DOM/Text.purs | 13 ++++-- src/Halogen/VDom/DOM/Types.purs | 14 ++++++- src/Halogen/VDom/DOM/Utils.purs | 73 ++++++++++++++++++--------------- src/Halogen/VDom/Util.purs | 10 ----- test/Hydration.purs | 3 +- 7 files changed, 71 insertions(+), 57 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 836df01..268c516 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -42,7 +42,7 @@ import Web.DOM.Node (Node) as DOM -- | machine3 ← Machine.step machine2 vdomTree3 -- | ... -- | ```` -hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Node -> VDomMachine a w +hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Element -> VDomMachine a w hydrateVDom spec rootNode = hydrate where build = buildVDom spec diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index d945bc4..16ced8a 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -20,6 +20,7 @@ import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM +import Web.DOM.Element (toNode) as DOM.Element import Web.DOM.Element as DOMElement import Web.DOM.Node (Node) as DOM @@ -41,12 +42,14 @@ hydrateElem (Array (VDom a w)) a w -hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do - checkNodeIsElementNode currentNode - checkNodeNamespaceIsEqualTo (toNullable ns1) currentNode - checkNodeNameIsEqualTo name1 currentNode - checkNodeChildrenLengthIsEqualTo (length ch1) currentNode +hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do + checkIsElementNode currentElement + checkTagNameIsEqualTo ns1 name1 currentElement + checkChildrenLengthIsEqualTo (length ch1) currentElement let + currentNode :: DOM.Node + currentNode = DOM.Element.toNode currentElement + onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) onChild = undefined children ← undefined diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index c9fc8ce..3e3080f 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -1,6 +1,7 @@ module Halogen.VDom.DOM.Text where import Halogen.VDom.DOM.Types +import Halogen.VDom.DOM.Utils import Prelude import Data.Array as Array @@ -16,9 +17,9 @@ import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, st import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util -import Halogen.VDom.DOM.Utils import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM +import Web.DOM.Element (toNode) as DOM.Element import Web.DOM.Element as DOMElement import Web.DOM.Node (Node) as DOM import Web.DOM.Node (textContent) @@ -37,9 +38,13 @@ buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do -- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w -hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → do - checkNodeIsTextNode currentNode - checkNodeTextContentIsEqTo s currentNode +hydrateText = EFn.mkEffectFn5 \currentElement (VDomSpec spec) _hydrate build s → do + let + currentNode :: DOM.Node + currentNode = DOM.Element.toNode currentElement + + checkIsTextNode currentElement + checkTextContentIsEqTo s currentElement let (state :: TextState a w) = { build, node: currentNode, value: s } pure $ mkStep $ Step currentNode state patchText haltText diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index c5d1b06..0783c57 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -31,7 +31,7 @@ type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomS type VDomHydrator i a w = EFn.EffectFn5 - DOM.Node + DOM.Element -- current element (VDomSpec a w) (VDomMachine a w) -- top hydrate function (VDomMachine a w) -- top build function @@ -40,7 +40,17 @@ type VDomHydrator i a w type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i j k l (VDomStep a w) -type VDomHydrator4 i j k l a w = EFn.EffectFn8 DOM.Node (VDomSpec a w) (VDomMachine a w) (VDomMachine a w) i j k l (VDomStep a w) +type VDomHydrator4 i j k l a w + = EFn.EffectFn8 + DOM.Element + (VDomSpec a w) + (VDomMachine a w) + (VDomMachine a w) + i + j + k + l + (VDomStep a w) -- | Widget machines recursively reference the configured spec to potentially -- | enable recursive trees of Widgets. diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Utils.purs index ff43168..c3b9c3e 100644 --- a/src/Halogen/VDom/DOM/Utils.purs +++ b/src/Halogen/VDom/DOM/Utils.purs @@ -6,7 +6,9 @@ import Prelude import Data.Array as Array import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) -import Data.Nullable (Nullable, toNullable) +import Data.Newtype (unwrap) +import Data.Nullable (Nullable, toMaybe, toNullable) +import Data.String (toUpper) import Data.Tuple (Tuple(..), fst) import Effect (Effect) import Effect.Exception (error, throwException) @@ -16,11 +18,14 @@ import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, st import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util +import Partial.Unsafe (unsafePartial) import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document (Document) as DOM -import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOMElement -import Web.DOM.Node (Node) as DOM +import Web.DOM as DOM +import Web.DOM.Document as DOM +import Web.DOM.Element as DOM +import Web.DOM.Element as DOM.Element +import Web.DOM.Node as DOM +import Web.DOM.NodeType as DOM.NodeType eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → @@ -37,44 +42,46 @@ quote s = "\"" <> s <> "\"" -------------------------------------- -- Text -checkNodeIsTextNode :: DOM.Node -> Effect Unit -checkNodeIsTextNode node = - EFn.runEffectFn1 Util.nodeIsTextNode node >>= if _ +getElementNodeType :: DOM.Element -> DOM.NodeType +getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode element) + +checkElementIsNodeType :: DOM.NodeType -> DOM.Element -> Effect Unit +checkElementIsNodeType expectedNodeType element = + let nodeType = getElementNodeType element + in if nodeType == expectedNodeType then pure unit else do - nodeType <- EFn.runEffectFn1 Util.getNodeType node - throwException (error $ "Expected node to be a text node (nodeType === 3), but got " <> show nodeType) + throwException (error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) + +checkIsTextNode :: DOM.Element -> Effect Unit +checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode -checkNodeTextContentIsEqTo :: String -> DOM.Node -> Effect Unit -checkNodeTextContentIsEqTo s node = do - textContent <- EFn.runEffectFn1 Util.getTextContent node - if textContent == s +checkTextContentIsEqTo :: String -> DOM.Element -> Effect Unit +checkTextContentIsEqTo expectedText element = do + textContent <- DOM.textContent (DOM.Element.toNode element) + if textContent == expectedText then pure unit - else throwException (error $ "Expected node text content to equal to " <> quote s <> ", but got " <> quote textContent) + else throwException (error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) -------------------------------------- -- Elem -checkNodeIsElementNode :: DOM.Node -> Effect Unit -checkNodeIsElementNode node = - EFn.runEffectFn1 Util.nodeIsElementNode node >>= if _ - then pure unit - else do - nodeType <- EFn.runEffectFn1 Util.getNodeType node - throwException (error $ "Expected node to be element node (nodeType === 1), but got " <> show nodeType) - -checkNodeNamespaceIsEqualTo :: Nullable Namespace -> DOM.Node -> Effect Unit -checkNodeNamespaceIsEqualTo namespace node = do - nullableNamespaceURI <- EFn.runEffectFn1 Util.getNamespaceURI node - if nullableNamespaceURI == namespace - then pure unit - else throwException (error $ "Expected node namespaceURI equal to " <> quote (show namespace) <> ", but got " <> quote (show nullableNamespaceURI)) +checkIsElementNode :: DOM.Element -> Effect Unit +checkIsElementNode = checkElementIsNodeType DOM.NodeType.ElementNode -checkNodeNameIsEqualTo :: ElemName -> DOM.Node -> Effect Unit -checkNodeNameIsEqualTo = undefined +checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit +checkTagNameIsEqualTo maybeNamespace elemName element = do + let + expectedTagName :: String + expectedTagName = + case maybeNamespace of + Just namespace -> toUpper $ unwrap namespace <> ":" <> unwrap elemName + Nothing -> toUpper $ unwrap elemName + let tagName = DOM.tagName element + when (tagName == expectedTagName) (throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) -checkNodeChildrenLengthIsEqualTo :: Int -> DOM.Node -> Effect Unit -checkNodeChildrenLengthIsEqualTo = undefined +checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit +checkChildrenLengthIsEqualTo = undefined undefined :: ∀ a . a undefined = unsafeCoerce unit diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 03bf2e2..97cc621 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -178,13 +178,3 @@ foreign import removeEventListener foreign import data JsUndefined ∷ Type foreign import jsUndefined ∷ JsUndefined - -foreign import getNodeType :: EFn.EffectFn1 DOM.Node Int - -foreign import nodeIsTextNode :: EFn.EffectFn1 DOM.Node Boolean - -foreign import nodeIsElementNode :: EFn.EffectFn1 DOM.Node Boolean - -foreign import getTextContent :: EFn.EffectFn1 DOM.Node String - -foreign import getNamespaceURI :: EFn.EffectFn1 DOM.Node (Nullable Namespace) diff --git a/test/Hydration.purs b/test/Hydration.purs index c5e063a..f781858 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -65,10 +65,9 @@ main = do let spec = mkSpec (DOM.toDocument doc) initialValue = initialState - appDivNode = DOM.toNode appDiv render = renderData initialVdom = un VDom (render initialValue) - machine ← EFn.runEffectFn1 (V.hydrateVDom spec appDivNode) initialVdom + machine ← EFn.runEffectFn1 (V.hydrateVDom spec appDiv) initialVdom listener ← DOM.eventListener \_ev → void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) From 4e387826958b115e26750f8e8e58e6714dfee89f Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 19:00:12 +0300 Subject: [PATCH 05/48] refactor: length --- src/Halogen/VDom/DOM/Utils.purs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Utils.purs index c3b9c3e..83746e6 100644 --- a/src/Halogen/VDom/DOM/Utils.purs +++ b/src/Halogen/VDom/DOM/Utils.purs @@ -25,6 +25,7 @@ import Web.DOM.Document as DOM import Web.DOM.Element as DOM import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM +import Web.DOM.NodeList (length) as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean @@ -48,10 +49,7 @@ getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode el checkElementIsNodeType :: DOM.NodeType -> DOM.Element -> Effect Unit checkElementIsNodeType expectedNodeType element = let nodeType = getElementNodeType element - in if nodeType == expectedNodeType - then pure unit - else do - throwException (error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) + in when (nodeType /= expectedNodeType) (throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) checkIsTextNode :: DOM.Element -> Effect Unit checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode @@ -59,9 +57,7 @@ checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode checkTextContentIsEqTo :: String -> DOM.Element -> Effect Unit checkTextContentIsEqTo expectedText element = do textContent <- DOM.textContent (DOM.Element.toNode element) - if textContent == expectedText - then pure unit - else throwException (error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) + when (textContent /= expectedText) (throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) -------------------------------------- -- Elem @@ -78,10 +74,13 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do Just namespace -> toUpper $ unwrap namespace <> ":" <> unwrap elemName Nothing -> toUpper $ unwrap elemName let tagName = DOM.tagName element - when (tagName == expectedTagName) (throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) + when (tagName /= expectedTagName) (throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit -checkChildrenLengthIsEqualTo = undefined +checkChildrenLengthIsEqualTo expectedLength element = do + (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) + elementChildrenLength <- DOM.NodeList.length elementChildren + when (elementChildrenLength /= expectedLength) (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) undefined :: ∀ a . a undefined = unsafeCoerce unit From e4557f4d19bebbd83427784ad31bf2b047a66948 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 20:47:37 +0300 Subject: [PATCH 06/48] refactor: wip --- src/Halogen/VDom/DOM.purs | 19 +++++++++-------- src/Halogen/VDom/DOM/Elem.purs | 35 ++++++++++++++++++++++---------- src/Halogen/VDom/DOM/Keyed.purs | 4 ++-- src/Halogen/VDom/DOM/Text.purs | 2 +- src/Halogen/VDom/DOM/Types.purs | 7 ++++--- src/Halogen/VDom/DOM/Utils.purs | 11 +++++++--- src/Halogen/VDom/DOM/Widget.purs | 2 +- src/Halogen/VDom/Util.js | 8 ++++++++ src/Halogen/VDom/Util.purs | 4 ++++ test/Hydration.purs | 10 +++++++-- 10 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 268c516..521d1bd 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -16,6 +16,7 @@ import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) +import Debug.Trace (traceM) import Effect.Uncurried as EFn import Foreign.Object as Object import Halogen.VDom.DOM.Elem (buildElem) as Export @@ -30,7 +31,7 @@ import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOMElement +import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM -- | Starts an initial `VDom` machine by providing a `VDomSpec`. @@ -43,15 +44,17 @@ import Web.DOM.Node (Node) as DOM -- | ... -- | ```` hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Element -> VDomMachine a w -hydrateVDom spec rootNode = hydrate +hydrateVDom spec rootNode = hydrate rootNode where build = buildVDom spec - hydrate = EFn.mkEffectFn1 case _ of - Text s → EFn.runEffectFn5 hydrateText rootNode spec hydrate build s - Elem namespace elemName attribute childrenVdoms → EFn.runEffectFn8 hydrateElem rootNode spec hydrate build namespace elemName attribute childrenVdoms - Keyed namespace elemName attribute keyedChildrenVdoms → undefined - Widget w → undefined - Grafted g → undefined + hydrate node = EFn.mkEffectFn1 \vdom -> do + traceM { message: "hydrateVDom", 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 + Keyed namespace elemName attribute keyedChildrenVdoms → undefined + Widget w → undefined + Grafted g → undefined buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 16ced8a..47fa169 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -1,15 +1,17 @@ module Halogen.VDom.DOM.Elem where +import Data.Tuple.Nested import Halogen.VDom.DOM.Types import Halogen.VDom.DOM.Utils import Prelude -import Data.Array (length) +import Data.Array (length, zip) import Data.Array as Array import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) +import Debug.Trace (traceM) import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn @@ -18,11 +20,14 @@ import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, st import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util -import Web.DOM.Document (Document) as DOM -import Web.DOM.Element (Element) as DOM -import Web.DOM.Element (toNode) as DOM.Element -import Web.DOM.Element as DOMElement -import Web.DOM.Node (Node) as DOM +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Document as DOM +import Web.DOM.Element as DOM +import Web.DOM.Element as DOM.Element +import Web.DOM.HTMLCollection (toArray) as DOM.HTMLCollection +import Web.DOM.Node as DOM +import Web.DOM.NodeList as DOM.NodeList +import Web.DOM.ParentNode as DOM.ParentNode type ElemState a w = { build ∷ VDomMachine a w @@ -44,16 +49,24 @@ hydrateElem w hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do checkIsElementNode currentElement + traceM { ns1, name1, as1, ch1 } checkTagNameIsEqualTo ns1 name1 currentElement checkChildrenLengthIsEqualTo (length ch1) currentElement let currentNode :: DOM.Node currentNode = DOM.Element.toNode currentElement - onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) - onChild = undefined - children ← undefined - attrs ← undefined + (currentElementChildren :: Array DOM.Element) <- DOM.ParentNode.children (DOM.Element.toParentNode currentElement) >>= DOM.HTMLCollection.toArray + traceM { currentElementChildren } + + let + onChild :: EFn.EffectFn2 Int (DOM.Element /\ (VDom a w)) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn2 \ix (element /\ child) → do + traceM { ix, element, child } + (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child + pure res + children ← EFn.runEffectFn2 Util.forE (zip currentElementChildren ch1) onChild + attrs ← EFn.runEffectFn1 (spec.buildAttributes currentElement) as1 let state = { build @@ -78,7 +91,7 @@ buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node - node = DOMElement.toNode el + node = DOM.Element.toNode el onChild :: EFn.EffectFn2 Int (VDom a w) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn2 \ix child → do diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index efbb23c..b63afb6 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -15,7 +15,7 @@ import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOMElement +import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM import Halogen.VDom.DOM.Types import Halogen.VDom.DOM.Utils @@ -35,7 +35,7 @@ buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node - node = DOMElement.toNode el + node = DOM.Element.toNode el onChild :: EFn.EffectFn3 String Int (Tuple String (VDom a w)) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn3 \k ix (Tuple _ vdom) → do diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index 3e3080f..cc7d4d3 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -20,7 +20,7 @@ import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element (toNode) as DOM.Element -import Web.DOM.Element as DOMElement +import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM import Web.DOM.Node (textContent) diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index 0783c57..9090c58 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -15,7 +15,7 @@ import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOMElement +import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM -- A function, that takes `VDom a w` and builds a `DOM.Node` @@ -33,7 +33,7 @@ type VDomHydrator i a w = EFn.EffectFn5 DOM.Element -- current element (VDomSpec a w) - (VDomMachine a w) -- top hydrate function + (DOM.Element -> VDomMachine a w) -- top hydrate function (VDomMachine a w) -- top build function i (VDomStep a w) @@ -44,7 +44,7 @@ type VDomHydrator4 i j k l a w = EFn.EffectFn8 DOM.Element (VDomSpec a w) - (VDomMachine a w) + (DOM.Element -> VDomMachine a w) (VDomMachine a w) i j @@ -64,6 +64,7 @@ newtype VDomSpec a w = VDomSpec -- what is handler -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 , buildAttributes ∷ DOM.Element → Machine a Unit + -- We need document to be able to call `document.createElement` function , document ∷ DOM.Document } diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Utils.purs index 83746e6..3381e39 100644 --- a/src/Halogen/VDom/DOM/Utils.purs +++ b/src/Halogen/VDom/DOM/Utils.purs @@ -1,5 +1,6 @@ module Halogen.VDom.DOM.Utils where +import Data.Tuple.Nested import Halogen.VDom.DOM.Types import Prelude @@ -24,9 +25,11 @@ import Web.DOM as DOM import Web.DOM.Document as DOM import Web.DOM.Element as DOM import Web.DOM.Element as DOM.Element +import Web.DOM.HTMLCollection (length) as DOM.HTMLCollection import Web.DOM.Node as DOM import Web.DOM.NodeList (length) as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType +import Web.DOM.ParentNode (children) as DOM.ParentNode eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → @@ -78,9 +81,11 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do - (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) - elementChildrenLength <- DOM.NodeList.length elementChildren - when (elementChildrenLength /= expectedLength) (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) + (elementChildren :: DOM.HTMLCollection) <- DOM.ParentNode.children (DOM.Element.toParentNode element) + elementChildrenLength <- DOM.HTMLCollection.length elementChildren + when (elementChildrenLength /= expectedLength) do + EFn.runEffectFn2 Util.warnAny "Error at " { element, elementChildren } + (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) undefined :: ∀ a . a undefined = unsafeCoerce unit diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index a79b48f..2180b04 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -15,7 +15,7 @@ import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOMElement +import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM import Halogen.VDom.DOM.Types diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index 116f489..834fb10 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -187,3 +187,11 @@ exports.getTextContent = function(el) { exports.getNamespaceURI = function(el) { return node.namespaceURI } + +exports.warnAny = function(message, x) { + console.warn(message, x) +} + +exports.logAny = function(message, x) { + console.log(message, x) +} diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 97cc621..533f90e 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -178,3 +178,7 @@ foreign import removeEventListener foreign import data JsUndefined ∷ Type foreign import jsUndefined ∷ JsUndefined + +foreign import warnAny ∷ ∀ a . EFn.EffectFn2 String a Unit + +foreign import logAny ∷ ∀ a . EFn.EffectFn2 String a Unit diff --git a/test/Hydration.purs b/test/Hydration.purs index f781858..3454f2a 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -19,13 +19,15 @@ import Halogen.VDom.Util (addEventListener) as Util import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=)) import Web.DOM.Element (Element) import Web.DOM.Element (toNode) as DOM +import Web.DOM.Element (toParentNode) as DOM.Element import Web.DOM.Node (Node, appendChild) as DOM import Web.DOM.ParentNode (ParentNode) +import Web.DOM.ParentNode (firstElementChild) as DOM.ParentNode import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM +import Web.Event.EventTarget (eventListener, 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 Web.Event.EventTarget (eventListener, EventListener) as DOM type State = Array { classes :: String, text :: String } @@ -60,6 +62,10 @@ main = do win ← DOM.window doc ← DOM.document win appDiv ← findRequiredElement "#app" (DOM.toParentNode doc) + + rootElement <- (appDiv # DOM.Element.toParentNode # DOM.ParentNode.firstElementChild) + >>= maybe (throwException (error $ "rootElement not found")) pure + updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc) let @@ -67,7 +73,7 @@ main = do initialValue = initialState render = renderData initialVdom = un VDom (render initialValue) - machine ← EFn.runEffectFn1 (V.hydrateVDom spec appDiv) initialVdom + machine ← EFn.runEffectFn1 (V.hydrateVDom spec rootElement) initialVdom listener ← DOM.eventListener \_ev → void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) From 8e99a8b178c0ca06d58c1b4a72f6ce61474b6afb Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 21:06:55 +0300 Subject: [PATCH 07/48] refactor: working without props --- hydration-test.html | 5 +---- src/Halogen/VDom/DOM/Elem.purs | 8 ++++---- src/Halogen/VDom/DOM/Utils.purs | 4 ++-- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/hydration-test.html b/hydration-test.html index 0ee7f9d..5c6f27c 100644 --- a/hydration-test.html +++ b/hydration-test.html @@ -15,10 +15,7 @@
-
-
test label 1
-
test label 2
-
+
test label 1
test label 2
diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 47fa169..952a03f 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -56,16 +56,16 @@ hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 currentNode :: DOM.Node currentNode = DOM.Element.toNode currentElement - (currentElementChildren :: Array DOM.Element) <- DOM.ParentNode.children (DOM.Element.toParentNode currentElement) >>= DOM.HTMLCollection.toArray - traceM { currentElementChildren } + (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray let + (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- TODO + onChild :: EFn.EffectFn2 Int (DOM.Element /\ (VDom a w)) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn2 \ix (element /\ child) → do - traceM { ix, element, child } (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child pure res - children ← EFn.runEffectFn2 Util.forE (zip currentElementChildren ch1) onChild + children ← EFn.runEffectFn2 Util.forE (zip currentElementChildren' ch1) onChild attrs ← EFn.runEffectFn1 (spec.buildAttributes currentElement) as1 let state = diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Utils.purs index 3381e39..4c14449 100644 --- a/src/Halogen/VDom/DOM/Utils.purs +++ b/src/Halogen/VDom/DOM/Utils.purs @@ -81,8 +81,8 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do - (elementChildren :: DOM.HTMLCollection) <- DOM.ParentNode.children (DOM.Element.toParentNode element) - elementChildrenLength <- DOM.HTMLCollection.length elementChildren + (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) + elementChildrenLength <- DOM.NodeList.length elementChildren when (elementChildrenLength /= expectedLength) do EFn.runEffectFn2 Util.warnAny "Error at " { element, elementChildren } (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) From bbf6ec21f8aeba8251c56fed2ee5775074d0b651 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 21:31:39 +0300 Subject: [PATCH 08/48] refactor: DOM.Prop -> move functions outside of buildProp, add el and emit to PropState --- src/Halogen/VDom/DOM/Prop.purs | 264 ++++++++++++++++++--------------- 1 file changed, 144 insertions(+), 120 deletions(-) diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 950f115..001fd43 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -93,12 +93,14 @@ propFromInt = unsafeCoerce propFromNumber ∷ Number → PropValue propFromNumber = unsafeCoerce -type EmitterInputBuilder a = DOM.Event -> Maybe a +type EmitterInputBuilder a = DOM.Event → Maybe a type EventListenerAndCurrentEmitterInputBuilder a = Tuple DOM.EventListener (Ref.Ref (EmitterInputBuilder a)) type PropState a = - { events :: Object.Object (EventListenerAndCurrentEmitterInputBuilder a) - , props :: Object.Object (Prop a) + { events ∷ Object.Object (EventListenerAndCurrentEmitterInputBuilder a) + , props ∷ Object.Object (Prop a) + , el ∷ DOM.Element + , emit ∷ a → Effect Unit } -- | A `Machine`` for applying attributes, properties, and event handlers. @@ -115,141 +117,163 @@ buildProp emit el = renderProp -- on next step - patches prop -- on halt - all ref watchers are notified that element is removed - renderProp :: EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) + renderProp ∷ EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) renderProp = EFn.mkEffectFn1 \ps1 → do - (events :: STObject' (EventListenerAndCurrentEmitterInputBuilder a)) ← Util.newMutMap + (events ∷ STObject' (EventListenerAndCurrentEmitterInputBuilder a)) ← Util.newMutMap -- for each prop in array: -- if prop is attr - set attr to element, store attr under "attr/XXX" key in a returned object -- if prop is property - set property to element, store property under "prop/XXX" key in a returned object -- if prop is handler for DOM.EventType - start listen and add listener to `events` mutable map, store handler under "handler/EVENTTYPE" in a returned object -- if prop is ref updater - store `emitterInputBuilder` in under a `ref` key in a returned object, call `emitter` on creation of all props (now) and on halt of all props (later) - (props :: Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (applyProp events) + (props ∷ Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn3 applyProp el emit events) let - (state :: PropState a) = + (state ∷ PropState a) = { events: Util.unsafeFreeze events , props + , el + , emit } pure $ mkStep $ Step unit state patchProp haltProp - patchProp :: - EFn.EffectFn2 - (PropState a) - (Array (Prop a)) - (Step (Array (Prop a)) Unit) - patchProp = EFn.mkEffectFn2 \state ps2 → do - events ← Util.newMutMap - let - { events: prevEvents, props: ps1 } = state - onThese = Fn.runFn2 diffProp prevEvents events - onThis = removeProp prevEvents - onThat = applyProp events - props ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ps1 ps2 propToStrKey onThese onThis onThat - let - nextState = - { events: Util.unsafeFreeze events - , props - } - pure $ mkStep $ Step unit nextState patchProp haltProp - - haltProp - :: EFn.EffectFn1 - (PropState a) - Unit - haltProp = EFn.mkEffectFn1 \state → do - case Object.lookup "ref" state.props of - Just (Ref emitterInputBuilder) → - EFn.runEffectFn1 mbEmit (emitterInputBuilder (Removed el)) - _ → pure unit +patchProp :: + ∀ a + . EFn.EffectFn2 + (PropState a) + (Array (Prop a)) + (Step (Array (Prop a)) Unit) +patchProp = EFn.mkEffectFn2 \state ps2 → do + events ← Util.newMutMap + let + { events: prevEvents, props: ps1, emit, el } = state + onThese = Fn.runFn3 diffProp el prevEvents events + onThis = Fn.runFn2 removeProp el prevEvents + onThat = Fn.runFn3 applyProp el emit events + props ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ps1 ps2 propToStrKey onThese onThis onThat + let + nextState = -- TODO: reuse prev object + { events: Util.unsafeFreeze events + , props + , el + , emit + } + pure $ mkStep $ Step unit nextState patchProp haltProp - mbEmit :: EFn.EffectFn1 (Maybe a) Unit - mbEmit = EFn.mkEffectFn1 case _ of - Just a → emit a +haltProp + ∷ ∀ a + . EFn.EffectFn1 + (PropState a) + Unit +haltProp = EFn.mkEffectFn1 \state → do + case Object.lookup "ref" state.props of + Just (Ref emitterInputBuilder) → + EFn.runEffectFn2 mbEmit state.emit (emitterInputBuilder (Removed state.el)) _ → pure unit - applyProp - :: STObject' (EventListenerAndCurrentEmitterInputBuilder a) - -> EFn.EffectFn3 String Int (Prop a) (Prop a) - applyProp events = EFn.mkEffectFn3 \_ _ v → - case v of - Attribute ns attr val → do - EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el - pure v - Property prop val → do - EFn.runEffectFn3 setProperty prop val el - pure v - Handler (DOM.EventType eventType) emitterInputBuilder → do - case Fn.runFn2 Util.unsafeGetAny eventType events of - -- if eventType is already present in events storage / listened - handler | Fn.runFn2 Util.unsafeHasAny eventType events → do - -- replace current event listener with new - Ref.write emitterInputBuilder (snd handler) - pure v - _ → do - ref ← Ref.new emitterInputBuilder - listener ← DOM.eventListener \ev → do - (emitterInputBuilder' :: EmitterInputBuilder a) ← Ref.read ref - EFn.runEffectFn1 mbEmit (emitterInputBuilder' ev) +mbEmit + ∷ ∀ a + . EFn.EffectFn2 + (a → Effect Unit) + (Maybe a) + Unit +mbEmit = EFn.mkEffectFn2 \emit ma → case ma of + Just a → emit a + _ → pure unit + +applyProp + ∷ ∀ a + . Fn.Fn3 + DOM.Element + (a → Effect Unit) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn3 String Int (Prop a) (Prop a)) +applyProp = Fn.mkFn3 \el emit events → EFn.mkEffectFn3 \_ _ v → + case v of + Attribute ns attr val → do + EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el + pure v + Property prop val → do + EFn.runEffectFn3 setProperty prop val el + pure v + Handler (DOM.EventType eventType) emitterInputBuilder → do + case Fn.runFn2 Util.unsafeGetAny eventType events of + -- if eventType is already present in events storage / listened + handler | Fn.runFn2 Util.unsafeHasAny eventType events → do + -- replace current event listener with new + Ref.write emitterInputBuilder (snd handler) + pure v + _ → do + ref ← Ref.new emitterInputBuilder + listener ← DOM.eventListener \ev → do + (emitterInputBuilder' ∷ EmitterInputBuilder a) ← Ref.read ref + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder' ev) - -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder - EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events + -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder + EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events - -- listen events of that type on the element - EFn.runEffectFn3 Util.addEventListener eventType listener el - pure v - Ref emitterInputBuilder → do - EFn.runEffectFn1 mbEmit (emitterInputBuilder (Created el)) - pure v + -- listen events of that type on the element + EFn.runEffectFn3 Util.addEventListener eventType listener el + pure v + Ref emitterInputBuilder → do + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder (Created el)) + pure v - diffProp - :: Fn.Fn2 - (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) - (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) - (EFn.EffectFn4 String Int (Prop a) (Prop a) (Prop a)) - diffProp = Fn.mkFn2 \prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → - case v1, v2 of - Attribute _ _ val1, Attribute ns2 attr2 val2 → - if val1 == val2 - then pure v2 - else do - EFn.runEffectFn4 Util.setAttribute (toNullable ns2) attr2 val2 el - pure v2 - Property _ val1, Property prop2 val2 → - case Fn.runFn2 Util.refEq val1 val2, prop2 of - true, _ → - pure v2 - _, "value" → do - let elVal = Fn.runFn2 unsafeGetProperty "value" el - if Fn.runFn2 Util.refEq elVal val2 - then pure v2 - else do - EFn.runEffectFn3 setProperty prop2 val2 el - pure v2 - _, _ → do - EFn.runEffectFn3 setProperty prop2 val2 el - pure v2 - Handler _ _, Handler (DOM.EventType ty) emitterInputBuilder → do - let - handler = Fn.runFn2 Util.unsafeLookup ty prevEvents - Ref.write emitterInputBuilder (snd handler) - EFn.runEffectFn3 Util.pokeMutMap ty handler events - pure v2 - _, _ → - pure v2 +diffProp + ∷ ∀ a + . Fn.Fn3 + DOM.Element + (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn4 String Int (Prop a) (Prop a) (Prop a)) +diffProp = Fn.mkFn3 \el prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → + case v1, v2 of + Attribute _ _ val1, Attribute ns2 attr2 val2 → + if val1 == val2 + then pure v2 + else do + EFn.runEffectFn4 Util.setAttribute (toNullable ns2) attr2 val2 el + pure v2 + Property _ val1, Property prop2 val2 → + case Fn.runFn2 Util.refEq val1 val2, prop2 of + true, _ → + pure v2 + _, "value" → do + let elVal = Fn.runFn2 unsafeGetProperty "value" el + if Fn.runFn2 Util.refEq elVal val2 + then pure v2 + else do + EFn.runEffectFn3 setProperty prop2 val2 el + pure v2 + _, _ → do + EFn.runEffectFn3 setProperty prop2 val2 el + pure v2 + Handler _ _, Handler (DOM.EventType ty) emitterInputBuilder → do + let + handler = Fn.runFn2 Util.unsafeLookup ty prevEvents + Ref.write emitterInputBuilder (snd handler) + EFn.runEffectFn3 Util.pokeMutMap ty handler events + pure v2 + _, _ → + pure v2 - removeProp :: Object.Object (EventListenerAndCurrentEmitterInputBuilder a) -> EFn.EffectFn2 String (Prop a) Unit - removeProp prevEvents = EFn.mkEffectFn2 \_ v → - case v of - Attribute ns attr _ → - EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el - Property prop _ → - EFn.runEffectFn2 removeProperty prop el - Handler (DOM.EventType ty) _ → do - let - handler = Fn.runFn2 Util.unsafeLookup ty prevEvents - EFn.runEffectFn3 Util.removeEventListener ty (fst handler) el - Ref _ → - pure unit +removeProp + ∷ ∀ a + . Fn.Fn2 + DOM.Element + (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn2 String (Prop a) Unit) +removeProp = Fn.mkFn2 \el prevEvents → EFn.mkEffectFn2 \_ v → + case v of + Attribute ns attr _ → + EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el + Property prop _ → + EFn.runEffectFn2 removeProperty prop el + Handler (DOM.EventType ty) _ → do + let + handler = Fn.runFn2 Util.unsafeLookup ty prevEvents + EFn.runEffectFn3 Util.removeEventListener ty (fst handler) el + Ref _ → + pure unit propToStrKey ∷ ∀ i. Prop i → String propToStrKey = case _ of @@ -267,8 +291,8 @@ unsafeGetProperty = Util.unsafeGetAny removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit removeProperty = EFn.mkEffectFn2 \key el → - EFn.runEffectFn3 Util.hasAttribute (null :: Nullable Namespace) key el >>= if _ -- If attr exists on element - then EFn.runEffectFn3 Util.removeAttribute (null :: Nullable Namespace) key el -- remove it using el.removeAttribute() + EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ -- If attr exists on element + then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el -- remove it using el.removeAttribute() else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el -- If it's property - set it to "" _ → case key of From c323602e9dab918efd2bcaae177a693a0f1daa38 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 14 May 2020 21:45:29 +0300 Subject: [PATCH 09/48] refactor: DOM.Prop -> move functions to different modules --- src/Halogen/VDom/DOM/Prop.purs | 223 ++---------------- src/Halogen/VDom/DOM/Prop/Implementation.purs | 129 ++++++++++ src/Halogen/VDom/DOM/Prop/Types.purs | 95 ++++++++ src/Halogen/VDom/DOM/Prop/Utils.purs | 48 ++++ 4 files changed, 286 insertions(+), 209 deletions(-) create mode 100644 src/Halogen/VDom/DOM/Prop/Implementation.purs create mode 100644 src/Halogen/VDom/DOM/Prop/Types.purs create mode 100644 src/Halogen/VDom/DOM/Prop/Utils.purs diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 001fd43..58315dc 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -1,11 +1,5 @@ module Halogen.VDom.DOM.Prop - ( Prop(..) - , ElemRef(..) - , PropValue - , propFromString - , propFromBoolean - , propFromInt - , propFromNumber + ( module Export , buildProp ) where @@ -30,78 +24,19 @@ import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM import Web.Event.EventTarget (eventListener, EventListener) as DOM --- | Attributes, properties, event handlers, and element lifecycles. --- | Parameterized by the type of handlers outputs. - --- | What is the difference between attributes and properties? --- | --- | Attributes are defined by HTML. Properties (on DOM elements) are defined by DOM. --- | E.g. `class` attribute corresponds to `element.className` property --- | almost always you should use properties on html elements, the svg elements don't have properties, only classes --- | more https://github.com/purescript-halogen/purescript-halogen-vdom/issues/30#issuecomment-518015764 --- | --- | Also, attributes can be only strings, props - strings, numbers, booleans -data Prop a - = Attribute - -- XML namespace - (Maybe Namespace) - -- Attribute name - String - -- Attribute value - String - | Property - -- Property name. Usually is equal to attribute name, exeptions are: "htmlFor" property is a "for" attribute, "className" - "class" - String - PropValue - | Handler - -- Event type to listen to - DOM.EventType - -- Function that builds input for emitter (EmitterInputBuilder), if Nothing is returned - emitter is not called - -- NOTE: If multiple event handlers are added for the same event for the same element - only last event handler is going to work - -- (e.g. like in `H.div [HP.eventHandler (...), HP.eventHandler (...)]`) - (DOM.Event → Maybe a) - | Ref - -- This function builds input for emitter function too, but when parent element is removed or created - -- If Nothing is returned - emitter is not called - -- NOTE: If multiple ref handlers are added for the same element - only last ref handler is going to work - (ElemRef DOM.Element → Maybe a) - -instance functorProp ∷ Functor Prop where - map f (Handler ty g) = Handler ty (map f <$> g) - map f (Ref g) = Ref (map f <$> g) - map f p = unsafeCoerce p - -data ElemRef a - = Created a - | Removed a - -instance functorElemRef ∷ Functor ElemRef where - map f (Created a) = Created (f a) - map f (Removed a) = Removed (f a) - -foreign import data PropValue ∷ Type - -propFromString ∷ String → PropValue -propFromString = unsafeCoerce - -propFromBoolean ∷ Boolean → PropValue -propFromBoolean = unsafeCoerce - -propFromInt ∷ Int → PropValue -propFromInt = unsafeCoerce - -propFromNumber ∷ Number → PropValue -propFromNumber = unsafeCoerce - -type EmitterInputBuilder a = DOM.Event → Maybe a -type EventListenerAndCurrentEmitterInputBuilder a = Tuple DOM.EventListener (Ref.Ref (EmitterInputBuilder a)) - -type PropState a = - { events ∷ Object.Object (EventListenerAndCurrentEmitterInputBuilder a) - , props ∷ Object.Object (Prop a) - , el ∷ DOM.Element - , emit ∷ a → Effect Unit - } +import Halogen.VDom.DOM.Prop.Implementation +import Halogen.VDom.DOM.Prop.Utils +import Halogen.VDom.DOM.Prop.Types +import Halogen.VDom.DOM.Prop.Types + ( Prop(..) + , ElemRef(..) + , PropValue + , propFromString + , propFromBoolean + , propFromInt + , propFromNumber + ) + as Export -- | A `Machine`` for applying attributes, properties, and event handlers. -- | An emitter effect must be provided to respond to events. For example, @@ -169,133 +104,3 @@ haltProp = EFn.mkEffectFn1 \state → do Just (Ref emitterInputBuilder) → EFn.runEffectFn2 mbEmit state.emit (emitterInputBuilder (Removed state.el)) _ → pure unit - -mbEmit - ∷ ∀ a - . EFn.EffectFn2 - (a → Effect Unit) - (Maybe a) - Unit -mbEmit = EFn.mkEffectFn2 \emit ma → case ma of - Just a → emit a - _ → pure unit - -applyProp - ∷ ∀ a - . Fn.Fn3 - DOM.Element - (a → Effect Unit) - (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) - (EFn.EffectFn3 String Int (Prop a) (Prop a)) -applyProp = Fn.mkFn3 \el emit events → EFn.mkEffectFn3 \_ _ v → - case v of - Attribute ns attr val → do - EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el - pure v - Property prop val → do - EFn.runEffectFn3 setProperty prop val el - pure v - Handler (DOM.EventType eventType) emitterInputBuilder → do - case Fn.runFn2 Util.unsafeGetAny eventType events of - -- if eventType is already present in events storage / listened - handler | Fn.runFn2 Util.unsafeHasAny eventType events → do - -- replace current event listener with new - Ref.write emitterInputBuilder (snd handler) - pure v - _ → do - ref ← Ref.new emitterInputBuilder - listener ← DOM.eventListener \ev → do - (emitterInputBuilder' ∷ EmitterInputBuilder a) ← Ref.read ref - EFn.runEffectFn2 mbEmit emit (emitterInputBuilder' ev) - - -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder - EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events - - -- listen events of that type on the element - EFn.runEffectFn3 Util.addEventListener eventType listener el - pure v - Ref emitterInputBuilder → do - EFn.runEffectFn2 mbEmit emit (emitterInputBuilder (Created el)) - pure v - -diffProp - ∷ ∀ a - . Fn.Fn3 - DOM.Element - (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) - (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) - (EFn.EffectFn4 String Int (Prop a) (Prop a) (Prop a)) -diffProp = Fn.mkFn3 \el prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → - case v1, v2 of - Attribute _ _ val1, Attribute ns2 attr2 val2 → - if val1 == val2 - then pure v2 - else do - EFn.runEffectFn4 Util.setAttribute (toNullable ns2) attr2 val2 el - pure v2 - Property _ val1, Property prop2 val2 → - case Fn.runFn2 Util.refEq val1 val2, prop2 of - true, _ → - pure v2 - _, "value" → do - let elVal = Fn.runFn2 unsafeGetProperty "value" el - if Fn.runFn2 Util.refEq elVal val2 - then pure v2 - else do - EFn.runEffectFn3 setProperty prop2 val2 el - pure v2 - _, _ → do - EFn.runEffectFn3 setProperty prop2 val2 el - pure v2 - Handler _ _, Handler (DOM.EventType ty) emitterInputBuilder → do - let - handler = Fn.runFn2 Util.unsafeLookup ty prevEvents - Ref.write emitterInputBuilder (snd handler) - EFn.runEffectFn3 Util.pokeMutMap ty handler events - pure v2 - _, _ → - pure v2 - -removeProp - ∷ ∀ a - . Fn.Fn2 - DOM.Element - (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) - (EFn.EffectFn2 String (Prop a) Unit) -removeProp = Fn.mkFn2 \el prevEvents → EFn.mkEffectFn2 \_ v → - case v of - Attribute ns attr _ → - EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el - Property prop _ → - EFn.runEffectFn2 removeProperty prop el - Handler (DOM.EventType ty) _ → do - let - handler = Fn.runFn2 Util.unsafeLookup ty prevEvents - EFn.runEffectFn3 Util.removeEventListener ty (fst handler) el - Ref _ → - pure unit - -propToStrKey ∷ ∀ i. Prop i → String -propToStrKey = case _ of - Attribute (Just (Namespace ns)) attr _ → "attr/" <> ns <> ":" <> attr - Attribute _ attr _ → "attr/:" <> attr - Property prop _ → "prop/" <> prop - Handler (DOM.EventType ty) _ → "handler/" <> ty - Ref _ → "ref" - -setProperty ∷ EFn.EffectFn3 String PropValue DOM.Element Unit -setProperty = Util.unsafeSetAny - -unsafeGetProperty ∷ Fn.Fn2 String DOM.Element PropValue -unsafeGetProperty = Util.unsafeGetAny - -removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit -removeProperty = EFn.mkEffectFn2 \key el → - EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ -- If attr exists on element - then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el -- remove it using el.removeAttribute() - else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of - "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el -- If it's property - set it to "" - _ → case key of - "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el - "colSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el - _ → EFn.runEffectFn3 Util.unsafeSetAny key Util.jsUndefined el diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs new file mode 100644 index 0000000..dc25a53 --- /dev/null +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -0,0 +1,129 @@ +module Halogen.VDom.DOM.Prop.Implementation where + +import Prelude + +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (null, toNullable, Nullable) +import Data.Tuple (Tuple(..), fst, snd) +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Uncurried as EFn +import Foreign (typeOf) +import Foreign.Object as Object +import Halogen.VDom as V +import Halogen.VDom.Machine (Step, Step'(..), mkStep) +import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Util as Util +import Halogen.VDom.Util (STObject') +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Element (Element) as DOM +import Web.Event.Event (EventType(..), Event) as DOM +import Web.Event.EventTarget (eventListener, EventListener) as DOM +import Halogen.VDom.DOM.Prop.Types +import Halogen.VDom.DOM.Prop.Utils + +applyProp + ∷ ∀ a + . Fn.Fn3 + DOM.Element + (a → Effect Unit) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn3 String Int (Prop a) (Prop a)) +applyProp = Fn.mkFn3 \el emit events → EFn.mkEffectFn3 \_ _ v → + case v of + Attribute ns attr val → do + EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el + pure v + Property prop val → do + EFn.runEffectFn3 setProperty prop val el + pure v + Handler (DOM.EventType eventType) emitterInputBuilder → do + case Fn.runFn2 Util.unsafeGetAny eventType events of + -- if eventType is already present in events storage / listened + handler | Fn.runFn2 Util.unsafeHasAny eventType events → do + -- replace current event listener with new + Ref.write emitterInputBuilder (snd handler) + pure v + _ → do + ref ← Ref.new emitterInputBuilder + listener ← DOM.eventListener \ev → do + (emitterInputBuilder' ∷ EmitterInputBuilder a) ← Ref.read ref + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder' ev) + + -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder + EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events + + -- listen events of that type on the element + EFn.runEffectFn3 Util.addEventListener eventType listener el + pure v + Ref emitterInputBuilder → do + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder (Created el)) + pure v + +mbEmit + ∷ ∀ a + . EFn.EffectFn2 + (a → Effect Unit) + (Maybe a) + Unit +mbEmit = EFn.mkEffectFn2 \emit ma → case ma of + Just a → emit a + _ → pure unit + +diffProp + ∷ ∀ a + . Fn.Fn3 + DOM.Element + (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn4 String Int (Prop a) (Prop a) (Prop a)) +diffProp = Fn.mkFn3 \el prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → + case v1, v2 of + Attribute _ _ val1, Attribute ns2 attr2 val2 → + if val1 == val2 + then pure v2 + else do + EFn.runEffectFn4 Util.setAttribute (toNullable ns2) attr2 val2 el + pure v2 + Property _ val1, Property prop2 val2 → + case Fn.runFn2 Util.refEq val1 val2, prop2 of + true, _ → + pure v2 + _, "value" → do + let elVal = Fn.runFn2 unsafeGetProperty "value" el + if Fn.runFn2 Util.refEq elVal val2 + then pure v2 + else do + EFn.runEffectFn3 setProperty prop2 val2 el + pure v2 + _, _ → do + EFn.runEffectFn3 setProperty prop2 val2 el + pure v2 + Handler _ _, Handler (DOM.EventType ty) emitterInputBuilder → do + let + handler = Fn.runFn2 Util.unsafeLookup ty prevEvents + Ref.write emitterInputBuilder (snd handler) + EFn.runEffectFn3 Util.pokeMutMap ty handler events + pure v2 + _, _ → + pure v2 + +removeProp + ∷ ∀ a + . Fn.Fn2 + DOM.Element + (Object.Object (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn2 String (Prop a) Unit) +removeProp = Fn.mkFn2 \el prevEvents → EFn.mkEffectFn2 \_ v → + case v of + Attribute ns attr _ → + EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el + Property prop _ → + EFn.runEffectFn2 removeProperty prop el + Handler (DOM.EventType ty) _ → do + let + handler = Fn.runFn2 Util.unsafeLookup ty prevEvents + EFn.runEffectFn3 Util.removeEventListener ty (fst handler) el + Ref _ → + pure unit diff --git a/src/Halogen/VDom/DOM/Prop/Types.purs b/src/Halogen/VDom/DOM/Prop/Types.purs new file mode 100644 index 0000000..b5c67da --- /dev/null +++ b/src/Halogen/VDom/DOM/Prop/Types.purs @@ -0,0 +1,95 @@ +module Halogen.VDom.DOM.Prop.Types where + +import Prelude + +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (null, toNullable, Nullable) +import Data.Tuple (Tuple(..), fst, snd) +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Uncurried as EFn +import Foreign (typeOf) +import Foreign.Object as Object +import Halogen.VDom as V +import Halogen.VDom.Machine (Step, Step'(..), mkStep) +import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Util as Util +import Halogen.VDom.Util (STObject') +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Element (Element) as DOM +import Web.Event.Event (EventType(..), Event) as DOM +import Web.Event.EventTarget (eventListener, EventListener) as DOM + +-- | Attributes, properties, event handlers, and element lifecycles. +-- | Parameterized by the type of handlers outputs. + +-- | What is the difference between attributes and properties? +-- | +-- | Attributes are defined by HTML. Properties (on DOM elements) are defined by DOM. +-- | E.g. `class` attribute corresponds to `element.className` property +-- | almost always you should use properties on html elements, the svg elements don't have properties, only classes +-- | more https://github.com/purescript-halogen/purescript-halogen-vdom/issues/30#issuecomment-518015764 +-- | +-- | Also, attributes can be only strings, props - strings, numbers, booleans +data Prop a + = Attribute + -- XML namespace + (Maybe Namespace) + -- Attribute name + String + -- Attribute value + String + | Property + -- Property name. Usually is equal to attribute name, exeptions are: "htmlFor" property is a "for" attribute, "className" - "class" + String + PropValue + | Handler + -- Event type to listen to + DOM.EventType + -- Function that builds input for emitter (EmitterInputBuilder), if Nothing is returned - emitter is not called + -- NOTE: If multiple event handlers are added for the same event for the same element - only last event handler is going to work + -- (e.g. like in `H.div [HP.eventHandler (...), HP.eventHandler (...)]`) + (DOM.Event → Maybe a) + | Ref + -- This function builds input for emitter function too, but when parent element is removed or created + -- If Nothing is returned - emitter is not called + -- NOTE: If multiple ref handlers are added for the same element - only last ref handler is going to work + (ElemRef DOM.Element → Maybe a) + +instance functorProp ∷ Functor Prop where + map f (Handler ty g) = Handler ty (map f <$> g) + map f (Ref g) = Ref (map f <$> g) + map f p = unsafeCoerce p + +data ElemRef a + = Created a + | Removed a + +instance functorElemRef ∷ Functor ElemRef where + map f (Created a) = Created (f a) + map f (Removed a) = Removed (f a) + +foreign import data PropValue ∷ Type + +propFromString ∷ String → PropValue +propFromString = unsafeCoerce + +propFromBoolean ∷ Boolean → PropValue +propFromBoolean = unsafeCoerce + +propFromInt ∷ Int → PropValue +propFromInt = unsafeCoerce + +propFromNumber ∷ Number → PropValue +propFromNumber = unsafeCoerce + +type EmitterInputBuilder a = DOM.Event → Maybe a +type EventListenerAndCurrentEmitterInputBuilder a = Tuple DOM.EventListener (Ref.Ref (EmitterInputBuilder a)) + +type PropState a = + { events ∷ Object.Object (EventListenerAndCurrentEmitterInputBuilder a) + , props ∷ Object.Object (Prop a) + , el ∷ DOM.Element + , emit ∷ a → Effect Unit + } diff --git a/src/Halogen/VDom/DOM/Prop/Utils.purs b/src/Halogen/VDom/DOM/Prop/Utils.purs new file mode 100644 index 0000000..3a7ec90 --- /dev/null +++ b/src/Halogen/VDom/DOM/Prop/Utils.purs @@ -0,0 +1,48 @@ +module Halogen.VDom.DOM.Prop.Utils where + +import Prelude + +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (null, toNullable, Nullable) +import Data.Tuple (Tuple(..), fst, snd) +import Effect (Effect) +import Effect.Ref as Ref +import Effect.Uncurried as EFn +import Foreign (typeOf) +import Foreign.Object as Object +import Halogen.VDom as V +import Halogen.VDom.Machine (Step, Step'(..), mkStep) +import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Util as Util +import Halogen.VDom.Util (STObject') +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Element (Element) as DOM +import Web.Event.Event (EventType(..), Event) as DOM +import Web.Event.EventTarget (eventListener, EventListener) as DOM +import Halogen.VDom.DOM.Prop.Types + +propToStrKey ∷ ∀ i. Prop i → String +propToStrKey = case _ of + Attribute (Just (Namespace ns)) attr _ → "attr/" <> ns <> ":" <> attr + Attribute _ attr _ → "attr/:" <> attr + Property prop _ → "prop/" <> prop + Handler (DOM.EventType ty) _ → "handler/" <> ty + Ref _ → "ref" + +setProperty ∷ EFn.EffectFn3 String PropValue DOM.Element Unit +setProperty = Util.unsafeSetAny + +unsafeGetProperty ∷ Fn.Fn2 String DOM.Element PropValue +unsafeGetProperty = Util.unsafeGetAny + +removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit +removeProperty = EFn.mkEffectFn2 \key el → + EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ -- If attr exists on element + then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el -- remove it using el.removeAttribute() + else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of + "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el -- If it's property - set it to "" + _ → case key of + "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el + "colSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el + _ → EFn.runEffectFn3 Util.unsafeSetAny key Util.jsUndefined el From 308f1466f45682cb42c5053459a9c8115eccb6c4 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 16 May 2020 12:59:06 +0300 Subject: [PATCH 10/48] feat: hydration -> wip, changed type of unsafeGetProperty from `Fn.Fn2 String DOM.Element PropValue` to `Fn.Fn2 String DOM.Element (Nullable PropValue)` --- src/Halogen/VDom/Attributes.js | 3 + src/Halogen/VDom/Attributes.purs | 23 +++ src/Halogen/VDom/DOM.purs | 3 +- .../VDom/DOM/{Utils.purs => Checkers.purs} | 27 +--- src/Halogen/VDom/DOM/Elem.purs | 5 +- src/Halogen/VDom/DOM/Keyed.purs | 4 +- src/Halogen/VDom/DOM/Prop.purs | 77 +++++++--- src/Halogen/VDom/DOM/Prop/Implementation.purs | 131 +++++++++++++----- src/Halogen/VDom/DOM/Prop/Utils.purs | 23 +-- src/Halogen/VDom/DOM/Text.purs | 23 +-- src/Halogen/VDom/DOM/Widget.purs | 19 +-- src/Halogen/VDom/Set.js | 19 +++ src/Halogen/VDom/Set.purs | 20 +++ src/Halogen/VDom/Util.js | 12 ++ src/Halogen/VDom/Util.purs | 30 +++- test/Hydration.purs | 109 ++++++++++++++- 16 files changed, 388 insertions(+), 140 deletions(-) create mode 100644 src/Halogen/VDom/Attributes.js create mode 100644 src/Halogen/VDom/Attributes.purs rename src/Halogen/VDom/DOM/{Utils.purs => Checkers.purs} (79%) create mode 100644 src/Halogen/VDom/Set.js create mode 100644 src/Halogen/VDom/Set.purs diff --git a/src/Halogen/VDom/Attributes.js b/src/Halogen/VDom/Attributes.js new file mode 100644 index 0000000..31d4bce --- /dev/null +++ b/src/Halogen/VDom/Attributes.js @@ -0,0 +1,3 @@ +exports.attributes = function (el) { + return el.attributes(); +}; diff --git a/src/Halogen/VDom/Attributes.purs b/src/Halogen/VDom/Attributes.purs new file mode 100644 index 0000000..804270b --- /dev/null +++ b/src/Halogen/VDom/Attributes.purs @@ -0,0 +1,23 @@ +module Halogen.VDom.Attributes where + +import Prelude + +import Halogen.VDom.Util +import Halogen.VDom.DOM.Checkers +import Effect (Effect) +import Web.DOM.Element as DOM +import Effect.Uncurried as EFn +import Unsafe.Coerce (unsafeCoerce) +import Halogen.VDom.Util as Util + +data NamedNodeMap + +foreign import attributes ∷ DOM.Element → NamedNodeMap + +forEachE + ∷ ∀ a + . EFn.EffectFn2 + NamedNodeMap + (EFn.EffectFn1 String Unit) + Unit +forEachE = unsafeCoerce Util.forEachE diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 521d1bd..65aadfb 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -23,12 +23,13 @@ import Halogen.VDom.DOM.Elem (buildElem) as Export import Halogen.VDom.DOM.Keyed (buildKeyed) as Export import Halogen.VDom.DOM.Text (buildText) as Export import Halogen.VDom.DOM.Types (VDomSpec(..)) as Export -import Halogen.VDom.DOM.Utils (undefined) +import Halogen.VDom.DOM.Checkers import Halogen.VDom.DOM.Widget (buildWidget) as Export import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util +import Halogen.VDom.Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element diff --git a/src/Halogen/VDom/DOM/Utils.purs b/src/Halogen/VDom/DOM/Checkers.purs similarity index 79% rename from src/Halogen/VDom/DOM/Utils.purs rename to src/Halogen/VDom/DOM/Checkers.purs index 4c14449..63ab29c 100644 --- a/src/Halogen/VDom/DOM/Utils.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -1,4 +1,4 @@ -module Halogen.VDom.DOM.Utils where +module Halogen.VDom.DOM.Checkers where import Data.Tuple.Nested import Halogen.VDom.DOM.Types @@ -18,7 +18,7 @@ import Foreign.Object as Object import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) -import Halogen.VDom.Util as Util +import Halogen.VDom.Util import Partial.Unsafe (unsafePartial) import Unsafe.Coerce (unsafeCoerce) import Web.DOM as DOM @@ -31,18 +31,6 @@ import Web.DOM.NodeList (length) as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType import Web.DOM.ParentNode (children) as DOM.ParentNode -eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean -eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → - if name1 == name2 - then case ns1, ns2 of - Just (Namespace ns1'), Just (Namespace ns2') | ns1' == ns2' → true - Nothing, Nothing → true - _, _ → false - else false - -quote :: String -> String -quote s = "\"" <> s <> "\"" - -------------------------------------- -- Text @@ -71,11 +59,9 @@ checkIsElementNode = checkElementIsNodeType DOM.NodeType.ElementNode checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit checkTagNameIsEqualTo maybeNamespace elemName element = do let + -- e.g. `DIV` or `FOO:SVG` expectedTagName :: String - expectedTagName = - case maybeNamespace of - Just namespace -> toUpper $ unwrap namespace <> ":" <> unwrap elemName - Nothing -> toUpper $ unwrap elemName + expectedTagName = toUpper $ fullAttributeName maybeNamespace elemName let tagName = DOM.tagName element when (tagName /= expectedTagName) (throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) @@ -84,8 +70,5 @@ checkChildrenLengthIsEqualTo expectedLength element = do (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) elementChildrenLength <- DOM.NodeList.length elementChildren when (elementChildrenLength /= expectedLength) do - EFn.runEffectFn2 Util.warnAny "Error at " { element, elementChildren } + EFn.runEffectFn2 warnAny "Error at " { element, elementChildren } (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) - -undefined :: ∀ a . a -undefined = unsafeCoerce unit diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 952a03f..ee1e6a2 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -2,7 +2,7 @@ module Halogen.VDom.DOM.Elem where import Data.Tuple.Nested import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Utils +import Halogen.VDom.DOM.Checkers import Prelude import Data.Array (length, zip) @@ -28,6 +28,7 @@ import Web.DOM.HTMLCollection (toArray) as DOM.HTMLCollection import Web.DOM.Node as DOM import Web.DOM.NodeList as DOM.NodeList import Web.DOM.ParentNode as DOM.ParentNode +-- | import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) type ElemState a w = { build ∷ VDomMachine a w @@ -117,7 +118,7 @@ patchElem = EFn.mkEffectFn2 \state vdom → do case vdom of Grafted g → EFn.runEffectFn2 patchElem state (runGraft g) - Elem ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → do -- if new vdom is elem AND new and old are equal + Elem ns2 name2 as2 ch2 | Fn.runFn4 Util.eqElemSpec ns1 name1 ns2 name2 → do -- if new vdom is elem AND new and old are equal case Array.length ch1, Array.length ch2 of 0, 0 → do attrs2 ← EFn.runEffectFn2 step attrs as2 diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index b63afb6..9fda361 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -18,7 +18,7 @@ import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Utils +import Halogen.VDom.DOM.Checkers type KeyedState a w = { build ∷ VDomMachine a w @@ -62,7 +62,7 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do case vdom of Grafted g → EFn.runEffectFn2 patchKeyed state (runGraft g) - Keyed ns2 name2 as2 ch2 | Fn.runFn4 eqElemSpec ns1 name1 ns2 name2 → + Keyed ns2 name2 as2 ch2 | Fn.runFn4 Util.eqElemSpec ns1 name1 ns2 name2 → case len1, Array.length ch2 of 0, 0 → do attrs2 ← EFn.runEffectFn2 Machine.step attrs as2 diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 58315dc..493ddfe 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -3,40 +3,71 @@ module Halogen.VDom.DOM.Prop , buildProp ) where -import Prelude +import Data.String.Common (joinWith) +import Halogen.VDom.DOM.Prop.Implementation (applyProp, diffProp, hydrateApplyProp, mbEmit, removeProp) +import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropState) +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 Data.Nullable (null, toNullable, Nullable) -import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect) -import Effect.Ref as Ref +import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Foreign (typeOf) import Foreign.Object as Object import Halogen.VDom as V +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.Types (Namespace(..)) +import Halogen.VDom.Set as Set import Halogen.VDom.Util as Util -import Halogen.VDom.Util (STObject') -import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM -import Web.Event.Event (EventType(..), Event) as DOM -import Web.Event.EventTarget (eventListener, EventListener) as DOM -import Halogen.VDom.DOM.Prop.Implementation -import Halogen.VDom.DOM.Prop.Utils -import Halogen.VDom.DOM.Prop.Types -import Halogen.VDom.DOM.Prop.Types - ( Prop(..) - , ElemRef(..) - , PropValue - , propFromString - , propFromBoolean - , propFromInt - , propFromNumber - ) - as Export +-- 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.mkSet + EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \name → EFn.runEffectFn2 Set.addSetMember name set) + pure set + +throwErrorIfExtraAttributeNamesNonEmpty ∷ Set.Set String → Effect Unit +throwErrorIfExtraAttributeNamesNonEmpty extraAttributeNames = do + when (Set.setSize extraAttributeNames > 0) + (do + throwException $ error $ "Extra attributes from the server: " <> (Set.setToArray extraAttributeNames # joinWith ", ") + ) + +hydrateProp + ∷ ∀ a + . (a → Effect Unit) + → DOM.Element + → V.Machine (Array (Prop a)) Unit +hydrateProp emit el = renderProp + where + renderProp ∷ EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) + renderProp = EFn.mkEffectFn1 \ps1 → do + (events ∷ STObject' (EventListenerAndCurrentEmitterInputBuilder a)) ← Util.newMutMap + + extraAttributeNames ← mkExtraAttributeNames el + + -- for each prop in array: + -- if prop is attr - dont set attr to element, store attr under "attr/XXX" key in a returned object + -- if prop is property - dont set property to element, store property under "prop/XXX" key in a returned object + -- if prop is handler for DOM.EventType - start listen and add listener to `events` mutable map, store handler under "handler/EVENTTYPE" in a returned object + -- if prop is ref updater - store `emitterInputBuilder` in under a `ref` key in a returned object, call `emitter` on creation of all props (now) and on halt of all props (later) + (props ∷ Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn4 hydrateApplyProp extraAttributeNames el emit events) + let + (state ∷ PropState a) = + { events: Util.unsafeFreeze events + , props + , el + , emit + } + pure $ mkStep $ Step unit state patchProp haltProp -- | A `Machine`` for applying attributes, properties, and event handlers. -- | An emitter effect must be provided to respond to events. For example, diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index dc25a53..d426ca9 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -1,27 +1,71 @@ module Halogen.VDom.DOM.Prop.Implementation where -import Prelude +import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropValue) +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 (null, toNullable, Nullable) +import Data.Nullable (toMaybe, toNullable) import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect) import Effect.Ref as Ref import Effect.Uncurried as EFn -import Foreign (typeOf) import Foreign.Object as Object -import Halogen.VDom as V -import Halogen.VDom.Machine (Step, Step'(..), mkStep) -import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Types (ElemName(..), Namespace) +import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) import Halogen.VDom.Util as Util -import Halogen.VDom.Util (STObject') -import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM import Web.Event.EventTarget (eventListener, EventListener) as DOM -import Halogen.VDom.DOM.Prop.Types -import Halogen.VDom.DOM.Prop.Utils +import Data.String.Common (toLower) +import Effect.Exception (error, throwException) +import Halogen.VDom.Set as Set + +checkAttributeExistsAndIsEqual ∷ Maybe Namespace → String → String → DOM.Element → Effect Unit +checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue element = do + elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe + case elementValue of + Nothing → 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 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 el = do + let propValue = Fn.runFn2 unsafeGetProperty "value" el + if Fn.runFn2 Util.refEq propValue expectedPropValue + then pure unit + else do + throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue) + +hydrateApplyProp + ∷ ∀ a + . Fn.Fn4 + (Set.Set String) + DOM.Element + (a → Effect Unit) + (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) + (EFn.EffectFn3 String Int (Prop a) (Prop a)) +hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffectFn3 \_ _ v → + case v of + Attribute maybeNamespace attributeName val → do + checkAttributeExistsAndIsEqual maybeNamespace attributeName val el + let fullAttributeName' = fullAttributeName maybeNamespace (ElemName attributeName) -- should be lowercased + EFn.runEffectFn2 Set.removeSetMember fullAttributeName' extraAttributeNames + pure v + Property propName val → do + checkPropExistsAndIsEqual propName val el + let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` + EFn.runEffectFn2 Set.removeSetMember fullAttributeName' extraAttributeNames + pure v + Handler eventType emitterInputBuilder → do + EFn.runEffectFn5 applyPropHandler el emit events eventType emitterInputBuilder + pure v + Ref emitterInputBuilder → do + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder (Created el)) + pure v applyProp ∷ ∀ a @@ -32,35 +76,46 @@ applyProp (EFn.EffectFn3 String Int (Prop a) (Prop a)) applyProp = Fn.mkFn3 \el emit events → EFn.mkEffectFn3 \_ _ v → case v of - Attribute ns attr val → do - EFn.runEffectFn4 Util.setAttribute (toNullable ns) attr val el + Attribute maybeNamespace attributeName val → do + EFn.runEffectFn4 Util.setAttribute (toNullable maybeNamespace) attributeName val el pure v - Property prop val → do - EFn.runEffectFn3 setProperty prop val el + Property propName val → do + EFn.runEffectFn3 setProperty propName val el + pure v + Handler eventType emitterInputBuilder → do + EFn.runEffectFn5 applyPropHandler el emit events eventType emitterInputBuilder pure v - Handler (DOM.EventType eventType) emitterInputBuilder → do - case Fn.runFn2 Util.unsafeGetAny eventType events of - -- if eventType is already present in events storage / listened - handler | Fn.runFn2 Util.unsafeHasAny eventType events → do - -- replace current event listener with new - Ref.write emitterInputBuilder (snd handler) - pure v - _ → do - ref ← Ref.new emitterInputBuilder - listener ← DOM.eventListener \ev → do - (emitterInputBuilder' ∷ EmitterInputBuilder a) ← Ref.read ref - EFn.runEffectFn2 mbEmit emit (emitterInputBuilder' ev) - - -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder - EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events - - -- listen events of that type on the element - EFn.runEffectFn3 Util.addEventListener eventType listener el - pure v Ref emitterInputBuilder → do EFn.runEffectFn2 mbEmit emit (emitterInputBuilder (Created el)) pure v +applyPropHandler + ∷ ∀ a + . EFn.EffectFn5 + DOM.Element + (a -> Effect Unit) + (STObject' (Tuple DOM.EventListener (Ref.Ref (DOM.Event -> Maybe a)))) + DOM.EventType + (DOM.Event -> Maybe a) + Unit +applyPropHandler = EFn.mkEffectFn5 \el emit events (DOM.EventType eventType) emitterInputBuilder → + case Fn.runFn2 Util.unsafeGetAny eventType events of + -- if eventType is already present in events storage / listened + handler | Fn.runFn2 Util.unsafeHasAny eventType events → do + -- replace current event listener with new + Ref.write emitterInputBuilder (snd handler) + _ → do + ref ← Ref.new emitterInputBuilder + listener ← DOM.eventListener \ev → do + (emitterInputBuilder' ∷ EmitterInputBuilder a) ← Ref.read ref + EFn.runEffectFn2 mbEmit emit (emitterInputBuilder' ev) + + -- set/add to events map, key is eventType, value contains element listener (so we can remove it on halt) AND current emitterInputBuilder + EFn.runEffectFn3 Util.pokeMutMap eventType (Tuple listener ref) events + + -- listen events of that type on the element + EFn.runEffectFn3 Util.addEventListener eventType listener el + mbEmit ∷ ∀ a . EFn.EffectFn2 @@ -90,6 +145,8 @@ diffProp = Fn.mkFn3 \el prevEvents events → EFn.mkEffectFn4 \_ _ v1 v2 → case Fn.runFn2 Util.refEq val1 val2, prop2 of true, _ → pure v2 + -- | In many browsers, though it may not be the case anymore, setting the input value always resets the cursor position/selection. + -- | This avoids setting it if it has not changed so as not to reset the cursor when you are typing. _, "value" → do let elVal = Fn.runFn2 unsafeGetProperty "value" el if Fn.runFn2 Util.refEq elVal val2 @@ -117,10 +174,10 @@ removeProp (EFn.EffectFn2 String (Prop a) Unit) removeProp = Fn.mkFn2 \el prevEvents → EFn.mkEffectFn2 \_ v → case v of - Attribute ns attr _ → - EFn.runEffectFn3 Util.removeAttribute (toNullable ns) attr el - Property prop _ → - EFn.runEffectFn2 removeProperty prop el + Attribute maybeNamespace attributeName _ → + EFn.runEffectFn3 Util.removeAttribute (toNullable maybeNamespace) attributeName el + Property propName _ → + EFn.runEffectFn2 removeProperty propName el Handler (DOM.EventType ty) _ → do let handler = Fn.runFn2 Util.unsafeLookup ty prevEvents diff --git a/src/Halogen/VDom/DOM/Prop/Utils.purs b/src/Halogen/VDom/DOM/Prop/Utils.purs index 3a7ec90..75e9d2c 100644 --- a/src/Halogen/VDom/DOM/Prop/Utils.purs +++ b/src/Halogen/VDom/DOM/Prop/Utils.purs @@ -1,26 +1,17 @@ module Halogen.VDom.DOM.Prop.Utils where -import Prelude +import Prelude (Unit, (<>), (>>=)) import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) -import Data.Nullable (null, toNullable, Nullable) -import Data.Tuple (Tuple(..), fst, snd) -import Effect (Effect) -import Effect.Ref as Ref +import Data.Nullable (Nullable, null) import Effect.Uncurried as EFn import Foreign (typeOf) -import Foreign.Object as Object -import Halogen.VDom as V -import Halogen.VDom.Machine (Step, Step'(..), mkStep) import Halogen.VDom.Types (Namespace(..)) import Halogen.VDom.Util as Util -import Halogen.VDom.Util (STObject') -import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM -import Web.Event.Event (EventType(..), Event) as DOM -import Web.Event.EventTarget (eventListener, EventListener) as DOM -import Halogen.VDom.DOM.Prop.Types +import Web.Event.Event (EventType(..)) as DOM +import Halogen.VDom.DOM.Prop.Types (Prop(..), PropValue) propToStrKey ∷ ∀ i. Prop i → String propToStrKey = case _ of @@ -33,15 +24,15 @@ propToStrKey = case _ of setProperty ∷ EFn.EffectFn3 String PropValue DOM.Element Unit setProperty = Util.unsafeSetAny -unsafeGetProperty ∷ Fn.Fn2 String DOM.Element PropValue +unsafeGetProperty ∷ Fn.Fn2 String DOM.Element (Nullable PropValue) unsafeGetProperty = Util.unsafeGetAny removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit removeProperty = EFn.mkEffectFn2 \key el → EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ -- If attr exists on element then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el -- remove it using el.removeAttribute() - else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of - "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el -- If it's property - set it to "" + else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of -- If it's property - set to following + "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el _ → case key of "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el "colSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index cc7d4d3..670089b 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -1,28 +1,15 @@ module Halogen.VDom.DOM.Text where -import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Utils -import Prelude +import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Checkers (checkIsTextNode, checkTextContentIsEqTo) +import Prelude (Unit, bind, discard, otherwise, pure, ($), (==)) -import Data.Array as Array -import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) -import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) -import Effect (Effect) -import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Machine (Step'(..), mkStep) +import Halogen.VDom.Types (VDom(..), runGraft) import Halogen.VDom.Util as Util -import Web.DOM.Document (Document) as DOM -import Web.DOM.Element (Element) as DOM -import Web.DOM.Element (toNode) as DOM.Element import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM -import Web.DOM.Node (textContent) type TextState a w = { build ∷ VDomMachine a w diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index 2180b04..83a1e87 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -1,23 +1,12 @@ module Halogen.VDom.DOM.Widget where -import Prelude +import Prelude (Unit, bind, discard, pure, (#), ($)) -import Data.Array as Array -import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) -import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) import Effect.Uncurried as EFn -import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) -import Halogen.VDom.Util as Util -import Web.DOM.Document (Document) as DOM -import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOM.Element +import Halogen.VDom.Machine (Step, Step'(..), halt, mkStep, step, unStep) +import Halogen.VDom.Types (VDom(..), runGraft) import Web.DOM.Node (Node) as DOM -import Halogen.VDom.DOM.Types +import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep) type WidgetState a w = { build ∷ VDomMachine a w diff --git a/src/Halogen/VDom/Set.js b/src/Halogen/VDom/Set.js new file mode 100644 index 0000000..ce40e96 --- /dev/null +++ b/src/Halogen/VDom/Set.js @@ -0,0 +1,19 @@ +exports.mkSet = function() { + return new Set() +} + +exports.removeSetMember = function(value, set) { + set.remove(value) +} + +exports.addSetMember = function(value, set) { + set.add(value) +} + +exports.setSize = function(set) { + return set.size +} + +exports.setToArray = function(set) { + return Array.from(set) +} diff --git a/src/Halogen/VDom/Set.purs b/src/Halogen/VDom/Set.purs new file mode 100644 index 0000000..402ad27 --- /dev/null +++ b/src/Halogen/VDom/Set.purs @@ -0,0 +1,20 @@ +module Halogen.VDom.Set where + +import Halogen.VDom.DOM.Checkers +import Halogen.VDom.Util +import Prelude + +import Effect (Effect) +import Effect.Uncurried (EffectFn2) as EFn + +data Set proxy + +foreign import mkSet ∷ ∀ a . Effect (Set a) + +foreign import removeSetMember ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit + +foreign import addSetMember ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit + +foreign import setSize ∷ ∀ a . Set a → Int + +foreign import setToArray ∷ ∀ a . Set a → Array a diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index 834fb10..01f83e1 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -158,6 +158,14 @@ exports.hasAttribute = function (ns, attr, el) { } }; +exports.getAttribute = function (ns, attr, el) { + if (ns != null) { + return el.getAttributeNS(ns, attr); + } else { + return el.getAttribute(attr); + } +}; + exports.addEventListener = function (ev, listener, el) { el.addEventListener(ev, listener, false); }; @@ -188,6 +196,10 @@ exports.getNamespaceURI = function(el) { return node.namespaceURI } +exports.anyToString = function (a) { + return a.toString(); +}; + exports.warnAny = function(message, x) { console.warn(message, x) } diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 533f90e..b1ca1c2 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -44,12 +44,14 @@ import Foreign.Object (Object) import Foreign.Object as Object import Foreign.Object.ST (STObject) import Foreign.Object.ST as STObject -import Halogen.VDom.Types (Namespace, ElemName) +import Halogen.VDom.Types import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Node (Node) as DOM import Web.Event.EventTarget (EventListener) as DOM +import Data.Maybe (Maybe(..)) +import Data.Newtype (unwrap) data STObject' a -- just like STObject, but without region @@ -169,6 +171,9 @@ foreign import removeAttribute foreign import hasAttribute ∷ EFn.EffectFn3 (Nullable Namespace) String DOM.Element Boolean +foreign import getAttribute + ∷ EFn.EffectFn3 (Nullable Namespace) String DOM.Element (Nullable String) + foreign import addEventListener ∷ EFn.EffectFn3 String DOM.EventListener DOM.Element Unit @@ -182,3 +187,26 @@ foreign import jsUndefined ∷ JsUndefined foreign import warnAny ∷ ∀ a . EFn.EffectFn2 String a Unit foreign import logAny ∷ ∀ a . EFn.EffectFn2 String a Unit + +undefined :: ∀ a . a +undefined = unsafeCoerce unit + +fullAttributeName ∷ Maybe Namespace → ElemName → String +fullAttributeName maybeNamespace elemName = + case maybeNamespace of + Just namespace -> unwrap namespace <> ":" <> unwrap elemName + Nothing -> unwrap elemName + +eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean +eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → + if name1 == name2 + then case ns1, ns2 of + Just (Namespace ns1'), Just (Namespace ns2') | ns1' == ns2' → true + Nothing, Nothing → true + _, _ → false + else false + +quote :: String -> String +quote s = "\"" <> s <> "\"" + +foreign import anyToString ∷ ∀ a . a → String diff --git a/test/Hydration.purs b/test/Hydration.purs index 3454f2a..9bce076 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -29,7 +29,7 @@ import Web.HTML (window) as DOM import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM import Web.HTML.Window (document) as DOM -type State = Array { classes :: String, text :: String } +type State = Array { classes ∷ String, text ∷ String } initialState ∷ State initialState = @@ -52,7 +52,7 @@ renderData st = [ "className" := elementState.classes ] [ text elementState.text ] -findRequiredElement :: String -> ParentNode -> Effect Element +findRequiredElement ∷ String → ParentNode → Effect Element findRequiredElement selector parentNode = DOM.querySelector (DOM.QuerySelector selector) parentNode >>= maybe (throwException (error $ selector <> " not found")) pure @@ -63,7 +63,7 @@ main = do doc ← DOM.document win appDiv ← findRequiredElement "#app" (DOM.toParentNode doc) - rootElement <- (appDiv # DOM.Element.toParentNode # DOM.ParentNode.firstElementChild) + rootElement ← (appDiv # DOM.Element.toParentNode # DOM.ParentNode.firstElementChild) >>= maybe (throwException (error $ "rootElement not found")) pure updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc) @@ -79,3 +79,106 @@ main = do void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) EFn.runEffectFn3 Util.addEventListener "click" listener updateStateButton + +tests ∷ Array { client ∷ String , errorMessage ∷ String , server ∷ String , title ∷ String } +tests = + -- | [ { title: "Attribute → renders" + [ { title: "Attribute → missing prop" + , server: """ +
test label 1
+ """ + , client: """ +
test label 1
+ """ + , errorMessage: """ + Warning: Prop `%s` did not match. Server: %s Client: %s%s + """ + } + , { title: "Attribute → extra prop" + , server: """ +
test label 1
+ """ + , client: """ +
test label 1
+ """ + , errorMessage: """ + Warning: Extra attributes from the server: %s%s + """ + } + , { title: "Attribute → did not match" + , server: """ +
test label 1
+ """ + , client: """ +
test label 1
+ """ + , errorMessage: """ + Warning: Prop `%s` did not match. Server: %s Client: %s%s + """ + } + -- | , { title: "Prop → boolean → " + , { title: "Prop → controlled element → renders" + , server: """ + + """ + , client: """ + + """ + , errorMessage: """ + """ + } + , { title: "Prop → controlled element → did not match → renders" + , server: """ + + """ + , client: """ + + """ + , errorMessage: """ + """ + } + ] + +-- | Having `` +-- | If do `$0.required = true` in chrome. +-- | THE html `` +-- | THE $0.attributes = { required: "true" } +-- | +-- | Having `` +-- | If do `$0.required = false` in chrome. +-- | THE html `` +-- | THE $0.attributes = {} +-- | +-- | thus, we should check prop is set and remove it from extraAttributeNames + +-- | Having `
` +-- | The `$0.attributes` +-- | `NamedNodeMap { id: "1", data-foo: "foo", data-bar: "bar", data-baz-bak: "baz-bak", someint: "1" }` +-- | The `$0.dataset` +-- | `DOMStringMap { foo: "foo", bar: "bar", bazBak: "baz-bak", someint: "1" }` +-- | +-- | Also, react doesnt support dataset property. Proof: +-- | server = `
` +-- | client = `
` +-- | errorMessage = `Prop `dataset` did not match. Server: "null" Client: "[object Object]"` +-- | +-- | thus, react does support `data-***` attributes, but doesn't support `dataset` property, so why bother with supporting `dataset` property? +-- | +-- | If we wont ignore dataset, then we sould implment something like +-- | +-- | data PropValue = PropValue_String String | PropValue_Int Int | ... | PropValue_Dataset (Object String) +-- | removePropFromExtraAttributeNames ∷ PropName → PropValue → Set → Set +-- | removePropFromExtraAttributeNames propName propValue set = +-- | if propName == "dataset" +-- | then forEach propValue +-- | (\key _val → do +-- | remove ("data-" <> camelCaseToKebabCase key) set +-- | ) +-- | else do +-- | remove (camelCaseToKebabCase key) set + +-- `$0.attributes` are: +-- is { required: "" } +-- is { required: "false" } +-- is { colspan: "1" }, but prop is colSpan = 1 +--
is { "foo:data-foo": "1" } From 477b9c43b14585d1f367cbba31b336b4e982f294 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 16 May 2020 14:37:48 +0300 Subject: [PATCH 11/48] feat: hydration -> works on test --- src/Halogen/VDom/Attributes.js | 2 +- src/Halogen/VDom/DOM/Checkers.purs | 4 ---- src/Halogen/VDom/DOM/Elem.purs | 6 +++--- src/Halogen/VDom/DOM/Keyed.purs | 1 + src/Halogen/VDom/DOM/Prop.purs | 21 ++++++++----------- src/Halogen/VDom/DOM/Prop/Implementation.purs | 6 +++--- src/Halogen/VDom/DOM/Prop/Types.purs | 10 +++++---- src/Halogen/VDom/DOM/Types.purs | 3 +++ src/Halogen/VDom/Set.js | 12 +++++------ src/Halogen/VDom/Set.purs | 10 ++++----- src/Halogen/VDom/Util.js | 2 +- test/TestVdom.purs | 12 +++++++++-- 12 files changed, 48 insertions(+), 41 deletions(-) diff --git a/src/Halogen/VDom/Attributes.js b/src/Halogen/VDom/Attributes.js index 31d4bce..72d6dce 100644 --- a/src/Halogen/VDom/Attributes.js +++ b/src/Halogen/VDom/Attributes.js @@ -1,3 +1,3 @@ exports.attributes = function (el) { - return el.attributes(); + return el.attributes; }; diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 63ab29c..591b448 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -1,11 +1,7 @@ module Halogen.VDom.DOM.Checkers where -import Data.Tuple.Nested -import Halogen.VDom.DOM.Types import Prelude -import Data.Array as Array -import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Newtype (unwrap) import Data.Nullable (Nullable, toMaybe, toNullable) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index ee1e6a2..54fa772 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -28,7 +28,7 @@ import Web.DOM.HTMLCollection (toArray) as DOM.HTMLCollection import Web.DOM.Node as DOM import Web.DOM.NodeList as DOM.NodeList import Web.DOM.ParentNode as DOM.ParentNode --- | import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) +import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) type ElemState a w = { build ∷ VDomMachine a w @@ -66,8 +66,8 @@ hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 onChild = EFn.mkEffectFn2 \ix (element /\ child) → do (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child pure res - children ← EFn.runEffectFn2 Util.forE (zip currentElementChildren' ch1) onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes currentElement) as1 + (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (zip currentElementChildren' ch1) onChild + (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let state = { build diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 9fda361..f1a1078 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -19,6 +19,7 @@ import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM import Halogen.VDom.DOM.Types import Halogen.VDom.DOM.Checkers +import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) type KeyedState a w = { build ∷ VDomMachine a w diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 493ddfe..2481369 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -1,6 +1,7 @@ module Halogen.VDom.DOM.Prop ( module Export , buildProp + , hydrateProp ) where import Data.String.Common (joinWith) @@ -16,13 +17,13 @@ import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Foreign.Object as Object -import Halogen.VDom as V 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.Machine (Step, Step'(..), mkStep, Machine) import Halogen.VDom.Set as Set import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM +import Halogen.VDom.DOM.Prop.Types (BuildPropFunction) -- inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030 mkExtraAttributeNames ∷ DOM.Element → Effect (Set.Set String) @@ -30,22 +31,20 @@ mkExtraAttributeNames el = do let namedNodeMap = Attributes.attributes el - (set ∷ Set.Set String) ← Set.mkSet - EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \name → EFn.runEffectFn2 Set.addSetMember name set) + (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.setSize extraAttributeNames > 0) + when (Set.size extraAttributeNames > 0) (do - throwException $ error $ "Extra attributes from the server: " <> (Set.setToArray extraAttributeNames # joinWith ", ") + throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") ) hydrateProp ∷ ∀ a - . (a → Effect Unit) - → DOM.Element - → V.Machine (Array (Prop a)) Unit + . BuildPropFunction a hydrateProp emit el = renderProp where renderProp ∷ EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) @@ -74,9 +73,7 @@ hydrateProp emit el = renderProp -- | to allow arbitrary effects in event handlers, one could use `id`. buildProp ∷ ∀ a - . (a → Effect Unit) -- emitter, for example the global broadcaster function for all elements in halogen component - → DOM.Element - → V.Machine (Array (Prop a)) Unit -- Machine takes array of properties for that element, outputs nothing + . BuildPropFunction a buildProp emit el = renderProp where -- what it does - creates a machine, that contains state diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index d426ca9..c454a62 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -34,7 +34,7 @@ checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue checkPropExistsAndIsEqual ∷ String → PropValue → DOM.Element → Effect Unit checkPropExistsAndIsEqual propName expectedPropValue el = do - let propValue = Fn.runFn2 unsafeGetProperty "value" el + let propValue = Fn.runFn2 unsafeGetProperty propName el if Fn.runFn2 Util.refEq propValue expectedPropValue then pure unit else do @@ -53,12 +53,12 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect Attribute maybeNamespace attributeName val → do checkAttributeExistsAndIsEqual maybeNamespace attributeName val el let fullAttributeName' = fullAttributeName maybeNamespace (ElemName attributeName) -- should be lowercased - EFn.runEffectFn2 Set.removeSetMember fullAttributeName' extraAttributeNames + EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames pure v Property propName val → do checkPropExistsAndIsEqual propName val el let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` - EFn.runEffectFn2 Set.removeSetMember fullAttributeName' extraAttributeNames + EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames pure v Handler eventType emitterInputBuilder → do EFn.runEffectFn5 applyPropHandler el emit events eventType emitterInputBuilder diff --git a/src/Halogen/VDom/DOM/Prop/Types.purs b/src/Halogen/VDom/DOM/Prop/Types.purs index b5c67da..af4b977 100644 --- a/src/Halogen/VDom/DOM/Prop/Types.purs +++ b/src/Halogen/VDom/DOM/Prop/Types.purs @@ -11,11 +11,8 @@ import Effect.Ref as Ref import Effect.Uncurried as EFn import Foreign (typeOf) import Foreign.Object as Object -import Halogen.VDom as V -import Halogen.VDom.Machine (Step, Step'(..), mkStep) +import Halogen.VDom.Machine (Step, Step'(..), mkStep, Machine) import Halogen.VDom.Types (Namespace(..)) -import Halogen.VDom.Util as Util -import Halogen.VDom.Util (STObject') import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM @@ -93,3 +90,8 @@ type PropState a = , el ∷ DOM.Element , emit ∷ a → Effect Unit } + +type BuildPropFunction a + = (a → Effect Unit) -- emitter, for example the global broadcaster function for all elements in halogen component + → DOM.Element + → Machine (Array (Prop a)) Unit -- Machine takes array of properties for that element, outputs nothing diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index 9090c58..e44a9b0 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -7,12 +7,14 @@ import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) +import Effect (Effect) import Effect.Uncurried as EFn import Foreign.Object as Object import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) import Halogen.VDom.Util as Util +import Halogen.VDom.DOM.Prop.Types (BuildPropFunction, Prop) import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element @@ -64,6 +66,7 @@ newtype VDomSpec a w = VDomSpec -- what is handler -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 , buildAttributes ∷ DOM.Element → Machine a Unit + , hydrateAttributes ∷ DOM.Element → Machine a Unit -- We need document to be able to call `document.createElement` function , document ∷ DOM.Document diff --git a/src/Halogen/VDom/Set.js b/src/Halogen/VDom/Set.js index ce40e96..0238524 100644 --- a/src/Halogen/VDom/Set.js +++ b/src/Halogen/VDom/Set.js @@ -1,19 +1,19 @@ -exports.mkSet = function() { +exports.empty = function() { return new Set() } -exports.removeSetMember = function(value, set) { - set.remove(value) +exports.delete = function(value, set) { + set.delete(value) } -exports.addSetMember = function(value, set) { +exports.add = function(value, set) { set.add(value) } -exports.setSize = function(set) { +exports.size = function(set) { return set.size } -exports.setToArray = function(set) { +exports.toArray = function(set) { return Array.from(set) } diff --git a/src/Halogen/VDom/Set.purs b/src/Halogen/VDom/Set.purs index 402ad27..8ae850e 100644 --- a/src/Halogen/VDom/Set.purs +++ b/src/Halogen/VDom/Set.purs @@ -9,12 +9,12 @@ import Effect.Uncurried (EffectFn2) as EFn data Set proxy -foreign import mkSet ∷ ∀ a . Effect (Set a) +foreign import empty ∷ ∀ a . Effect (Set a) -foreign import removeSetMember ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit +foreign import delete ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit -foreign import addSetMember ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit +foreign import add ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit -foreign import setSize ∷ ∀ a . Set a → Int +foreign import size ∷ ∀ a . Set a → Int -foreign import setToArray ∷ ∀ a . Set a → Array a +foreign import toArray ∷ ∀ a . Set a → Array a diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index 01f83e1..12e8a80 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -197,7 +197,7 @@ exports.getNamespaceURI = function(el) { } exports.anyToString = function (a) { - return a.toString(); + return String(a); }; exports.warnAny = function(message, x) { diff --git a/test/TestVdom.purs b/test/TestVdom.purs index 1827802..83b09ae 100644 --- a/test/TestVdom.purs +++ b/test/TestVdom.purs @@ -8,10 +8,14 @@ import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype, un) import Data.Tuple (Tuple) import Halogen.VDom as V -import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp) +import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp, hydrateProp) +import Halogen.VDom.DOM.Prop.Types (BuildPropFunction) import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk) import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document (Document) as DOM +import Web.DOM.Document as DOM +import Web.DOM.Element (Element) as DOM +import Halogen.VDom.Machine (Machine) +import Effect (Effect) infixr 1 prop as := @@ -37,11 +41,15 @@ text a = VDom $ V.Text a thunk ∷ ∀ a b. (a → VDom b) → a → VDom b thunk render val = VDom $ V.Widget $ Fn.runFn2 thunk1 render val +myfn :: ((Void → Effect Unit) -> DOM.Element -> Machine (Array (Prop Void)) Unit) → DOM.Element → Machine (Array (Prop Void)) Unit +myfn buildProp element = buildProp (const (pure unit)) element + mkSpec ∷ DOM.Document → V.VDomSpec (Array (Prop Void)) (Thunk VDom Void) mkSpec document = V.VDomSpec { buildWidget: buildThunk (un VDom) , buildAttributes: buildProp (const (pure unit)) + , hydrateAttributes: hydrateProp (const (pure unit)) , document } From 8ccb49210f7c0c2b540c2a0b335da37ad25bc6ab Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 16 May 2020 15:13:22 +0300 Subject: [PATCH 12/48] feat: hydration -> impl for keyed, not tested --- src/Halogen/VDom/DOM.purs | 4 +-- src/Halogen/VDom/DOM/Elem.purs | 7 ++--- src/Halogen/VDom/DOM/Keyed.purs | 50 ++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 65aadfb..e237a9d 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -53,9 +53,9 @@ hydrateVDom spec rootNode = hydrate rootNode 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 - Keyed namespace elemName attribute keyedChildrenVdoms → undefined + Keyed namespace elemName attribute keyedChildrenVdoms → EFn.runEffectFn8 hydrateKeyed node spec hydrate build namespace elemName attribute keyedChildrenVdoms Widget w → undefined - Grafted g → undefined + Grafted g → EFn.runEffectFn1 (hydrate node) (runGraft g) buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 54fa772..7cd99df 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -5,7 +5,7 @@ import Halogen.VDom.DOM.Types import Halogen.VDom.DOM.Checkers import Prelude -import Data.Array (length, zip) +import Data.Array (length, zip) as Array import Data.Array as Array import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) @@ -50,9 +50,8 @@ hydrateElem w hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do checkIsElementNode currentElement - traceM { ns1, name1, as1, ch1 } checkTagNameIsEqualTo ns1 name1 currentElement - checkChildrenLengthIsEqualTo (length ch1) currentElement + checkChildrenLengthIsEqualTo (Array.length ch1) currentElement let currentNode :: DOM.Node currentNode = DOM.Element.toNode currentElement @@ -66,7 +65,7 @@ hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 onChild = EFn.mkEffectFn2 \ix (element /\ child) → do (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child pure res - (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (zip currentElementChildren' ch1) onChild + (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren' ch1) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let state = diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index f1a1078..6500dfa 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -16,10 +16,13 @@ import Halogen.VDom.Util as Util import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.Node (Node) as DOM +import Web.DOM.NodeList as DOM.NodeList +import Unsafe.Coerce (unsafeCoerce) +import Web.DOM.Node as DOM import Halogen.VDom.DOM.Types import Halogen.VDom.DOM.Checkers import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) +import Data.Tuple.Nested type KeyedState a w = { build ∷ VDomMachine a w @@ -31,6 +34,51 @@ type KeyedState a w = , length ∷ Int } +hydrateKeyed + ∷ ∀ a w + . VDomHydrator4 + (Maybe Namespace) + ElemName + a + (Array (Tuple String (VDom a w))) + a + w +hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do + checkIsElementNode currentElement + checkTagNameIsEqualTo ns1 name1 currentElement + checkChildrenLengthIsEqualTo (Array.length keyedChildren) currentElement + let + currentNode :: DOM.Node + currentNode = DOM.Element.toNode currentElement + + (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray + + let + (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- TODO + + onChild :: EFn.EffectFn3 String Int ({ element ∷ DOM.Element, keyedChild ∷ Tuple String (VDom a w) }) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn3 \k ix ({ element, keyedChild: Tuple _ child }) → do + (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child + pure res + (children :: Object.Object (Step (VDom a w) DOM.Node)) ← + EFn.runEffectFn3 + Util.strMapWithIxE + (Array.zipWith (\element keyedChild → { element, keyedChild }) currentElementChildren' keyedChildren) + (\{ keyedChild } → fst keyedChild) + onChild + (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 + let + state = + { build + , node: currentNode + , attrs + , ns: ns1 + , name: name1 + , children + , length: Array.length keyedChildren + } + pure $ mkStep $ Step currentNode state patchKeyed haltKeyed + buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document From 7689b19ab4b15858da50603f0fcf8a3dc433659a Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 16 May 2020 15:38:33 +0300 Subject: [PATCH 13/48] feat: test keyed implementation --- test/Hydration.purs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/test/Hydration.purs b/test/Hydration.purs index 9bce076..5cd6649 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -29,23 +29,24 @@ import Web.HTML (window) as DOM import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM import Web.HTML.Window (document) as DOM -type State = Array { classes ∷ String, text ∷ String } +type State = Array { classes ∷ String, text ∷ String, key ∷ String } initialState ∷ State initialState = - [ { classes: "label1", text: "test label 1" } - , { classes: "label2", text: "test label 2" } + [ { classes: "label1", text: "test label 1", key: "1" } + , { classes: "label2", text: "test label 2", key: "2" } ] state2 ∷ State state2 = - [ { classes: "label2", text: "test label 1.1" } - , { classes: "label1", text: "test label 2.1" } + [ { classes: "label2", text: "test label 1.1", key: "1" } + , { classes: "label1", text: "test label 2.1", key: "2" } ] renderData ∷ State → VDom Void -renderData st = - elem "div" [ "className" := "component" ] (st <#> renderElement) +renderData stateArray = + -- | keyed "div" [ "className" := "component" ] (stateArray <#> renderElement) + keyed "div" [ "className" := "component" ] (map (\state → Tuple state.key (renderElement state)) stateArray) where renderElement elementState = elem "div" From ad6811311e6cfb0c596eef496f097e94edded60a Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 16 May 2020 19:08:38 +0300 Subject: [PATCH 14/48] feat: hydration -> implement for widgets and thunks --- src/Halogen/VDom/DOM.purs | 3 +-- src/Halogen/VDom/DOM/Text.purs | 12 ++++++------ src/Halogen/VDom/DOM/Types.purs | 1 + src/Halogen/VDom/DOM/Widget.purs | 11 ++++++++++- src/Halogen/VDom/Thunk.purs | 26 ++++++++++++++++++++++++-- test/Hydration.purs | 3 ++- test/TestVdom.purs | 3 ++- 7 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index e237a9d..6b7dfbb 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -49,12 +49,11 @@ hydrateVDom spec rootNode = hydrate rootNode where build = buildVDom spec hydrate node = EFn.mkEffectFn1 \vdom -> do - traceM { message: "hydrateVDom", 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 Keyed namespace elemName attribute keyedChildrenVdoms → EFn.runEffectFn8 hydrateKeyed node spec hydrate build namespace elemName attribute keyedChildrenVdoms - Widget w → undefined + Widget w → EFn.runEffectFn5 hydrateWidget node spec hydrate build w Grafted g → EFn.runEffectFn1 (hydrate node) (runGraft g) buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index 670089b..dee0e1f 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -17,12 +17,6 @@ type TextState a w = , value ∷ String } -buildText ∷ ∀ a w. VDomBuilder String a w -buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do - node ← EFn.runEffectFn2 Util.createTextNode s spec.document - let (state :: TextState a w) = { build, node, value: s } - pure $ mkStep $ Step node state patchText haltText - -- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w hydrateText = EFn.mkEffectFn5 \currentElement (VDomSpec spec) _hydrate build s → do @@ -35,6 +29,12 @@ hydrateText = EFn.mkEffectFn5 \currentElement (VDomSpec spec) _hydrate build s let (state :: TextState a w) = { build, node: currentNode, value: s } pure $ mkStep $ Step currentNode state patchText haltText +buildText ∷ ∀ a w. VDomBuilder String a w +buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do + node ← EFn.runEffectFn2 Util.createTextNode s spec.document + let (state :: TextState a w) = { build, node, value: s } + pure $ mkStep $ Step node state patchText haltText + patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) patchText = EFn.mkEffectFn2 \state newVdom → do let { build, node, value: value1 } = state diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index e44a9b0..282da12 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -58,6 +58,7 @@ type VDomHydrator4 i j k l a w -- | enable recursive trees of Widgets. newtype VDomSpec a w = VDomSpec { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` + , hydrateWidget ∷ VDomSpec a w → DOM.Element → Machine w DOM.Node -- example: -- buildAttributes = buildProps handler diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index 83a1e87..539622a 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -6,13 +6,22 @@ import Effect.Uncurried as EFn import Halogen.VDom.Machine (Step, Step'(..), halt, mkStep, step, unStep) import Halogen.VDom.Types (VDom(..), runGraft) import Web.DOM.Node (Node) as DOM -import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep, VDomHydrator) type WidgetState a w = { build ∷ VDomMachine a w , widget ∷ Step w DOM.Node } +hydrateWidget ∷ ∀ a w. VDomHydrator w a w +hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpec spec) _hydrate build w → do + res ← EFn.runEffectFn1 (spec.hydrateWidget (VDomSpec spec) elem) w + let + res' :: Step (VDom a w) DOM.Node + res' = res # unStep \(Step n s k1 k2) → + mkStep $ Step n { build, widget: res } patchWidget haltWidget + pure res' + buildWidget ∷ ∀ a w. VDomBuilder w a w buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 013a0f6..69676cb 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -1,6 +1,7 @@ module Halogen.VDom.Thunk ( Thunk , buildThunk + , hydrateThunk , runThunk , hoist , mapThunk @@ -19,6 +20,11 @@ import Halogen.VDom.Machine as M import Halogen.VDom.Util as Util import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Node (Node) +import Halogen.VDom.DOM.Types (VDomMachine) +import Web.DOM.Document (Document) as DOM +import Web.DOM.Element (Element) as DOM +import Web.DOM.Element as DOM.Element +import Web.DOM.Node (Node) as DOM foreign import data ThunkArg ∷ Type @@ -97,16 +103,32 @@ type ThunkState f i a w = , vdom ∷ M.Step (V.VDom a w) Node } +hydrateThunk + ∷ ∀ f i a w + . (f i → V.VDom a w) + → V.VDomSpec a w + → DOM.Element + → V.Machine (Thunk f i) Node +hydrateThunk toVDom spec element = mkThunkBuilder (\spec → V.hydrateVDom spec element) toVDom spec + buildThunk ∷ ∀ f i a w . (f i → V.VDom a w) → V.VDomSpec a w → V.Machine (Thunk f i) Node -buildThunk toVDom = renderThunk +buildThunk = mkThunkBuilder V.buildVDom + +mkThunkBuilder + ∷ ∀ f i a w + . (V.VDomSpec a w → VDomMachine a w) + → (f i → V.VDom a w) + → V.VDomSpec a w + → V.Machine (Thunk f i) Node +mkThunkBuilder buildVDom toVDom = renderThunk where renderThunk ∷ V.VDomSpec a w → V.Machine (Thunk f i) Node renderThunk spec = EFn.mkEffectFn1 \t → do - vdom ← EFn.runEffectFn1 (V.buildVDom spec) (toVDom (runThunk t)) + vdom ← EFn.runEffectFn1 (buildVDom spec) (toVDom (runThunk t)) pure $ M.mkStep $ M.Step (M.extract vdom) { thunk: t, vdom } patchThunk haltThunk patchThunk ∷ EFn.EffectFn2 (ThunkState f i a w) (Thunk f i) (V.Step (Thunk f i) Node) diff --git a/test/Hydration.purs b/test/Hydration.purs index 5cd6649..63659d0 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -46,7 +46,8 @@ state2 = renderData ∷ State → VDom Void renderData stateArray = -- | keyed "div" [ "className" := "component" ] (stateArray <#> renderElement) - keyed "div" [ "className" := "component" ] (map (\state → Tuple state.key (renderElement state)) stateArray) + -- | keyed "div" [ "className" := "component" ] (map (\state → Tuple state.key (renderElement state)) stateArray) + keyed "div" [ "className" := "component" ] (map (\state → Tuple state.key (thunk renderElement state)) stateArray) where renderElement elementState = elem "div" diff --git a/test/TestVdom.purs b/test/TestVdom.purs index 83b09ae..471886f 100644 --- a/test/TestVdom.purs +++ b/test/TestVdom.purs @@ -10,7 +10,7 @@ import Data.Tuple (Tuple) import Halogen.VDom as V import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp, hydrateProp) import Halogen.VDom.DOM.Prop.Types (BuildPropFunction) -import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk) +import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk, hydrateThunk) import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Document as DOM import Web.DOM.Element (Element) as DOM @@ -49,6 +49,7 @@ mkSpec → V.VDomSpec (Array (Prop Void)) (Thunk VDom Void) mkSpec document = V.VDomSpec { buildWidget: buildThunk (un VDom) + , hydrateWidget: hydrateThunk (un VDom) , buildAttributes: buildProp (const (pure unit)) , hydrateAttributes: hydrateProp (const (pure unit)) , document From 40494f7d756213b9b7d0567140fef59e59df507f Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 2 Jun 2020 14:27:01 +0300 Subject: [PATCH 15/48] feat: props -> move checkers to separate module --- src/Halogen/VDom/DOM/Prop/Checkers.purs | 34 +++++++++++++++++++ src/Halogen/VDom/DOM/Prop/Implementation.purs | 30 ++++------------ 2 files changed, 40 insertions(+), 24 deletions(-) create mode 100644 src/Halogen/VDom/DOM/Prop/Checkers.purs diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs new file mode 100644 index 0000000..38f5c1a --- /dev/null +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -0,0 +1,34 @@ +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, ($), (<#>), (<>), (==)) + +import Data.Function.Uncurried as Fn +import Data.Maybe (Maybe(..)) +import Data.Nullable (toMaybe, toNullable) +import Effect (Effect) +import Effect.Uncurried as EFn +import Halogen.VDom.Types (ElemName(..), Namespace) +import Halogen.VDom.Util (anyToString, fullAttributeName, quote) +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 + elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe + case elementValue of + Nothing → 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 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 el = do + let propValue = Fn.runFn2 unsafeGetProperty propName el + if Fn.runFn2 Util.refEq propValue expectedPropValue + then pure unit + else do + throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index c454a62..58d83f3 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -1,45 +1,27 @@ module Halogen.VDom.DOM.Prop.Implementation where -import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropValue) +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 Prelude (Unit, bind, discard, pure, unit, (==)) import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) -import Data.Nullable (toMaybe, toNullable) +import Data.Nullable (toNullable) 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.Types (ElemName(..), Namespace) -import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) +import Halogen.VDom.Types (ElemName(..)) +import Halogen.VDom.Util (STObject', fullAttributeName) import Halogen.VDom.Util as Util 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 Effect.Exception (error, throwException) import Halogen.VDom.Set as Set -checkAttributeExistsAndIsEqual ∷ Maybe Namespace → String → String → DOM.Element → Effect Unit -checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue element = do - elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe - case elementValue of - Nothing → 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 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 el = do - let propValue = Fn.runFn2 unsafeGetProperty propName el - if Fn.runFn2 Util.refEq propValue expectedPropValue - then pure unit - else do - throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue) - hydrateApplyProp ∷ ∀ a . Fn.Fn4 From 676460371760da418ac43161e1cbc18618cf4576 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 2 Jun 2020 15:11:02 +0300 Subject: [PATCH 16/48] refactor: warnings -> resolve --- src/Halogen/VDom/Attributes.purs | 8 ++---- src/Halogen/VDom/DOM.purs | 28 ++++--------------- src/Halogen/VDom/DOM/Checkers.purs | 36 ++++++++++-------------- src/Halogen/VDom/DOM/Elem.purs | 27 ++++++------------ src/Halogen/VDom/DOM/Keyed.purs | 17 +++++------- src/Halogen/VDom/DOM/Prop.purs | 5 ++-- src/Halogen/VDom/DOM/Prop/Types.purs | 16 ++++------- src/Halogen/VDom/DOM/Types.purs | 15 ++-------- src/Halogen/VDom/Set.purs | 4 +-- src/Halogen/VDom/Thunk.purs | 5 +--- src/Halogen/VDom/Util.js | 20 -------------- src/Halogen/VDom/Util.purs | 41 ++-------------------------- test/Hydration.purs | 14 ++-------- test/TestVdom.purs | 3 +- 14 files changed, 57 insertions(+), 182 deletions(-) diff --git a/src/Halogen/VDom/Attributes.purs b/src/Halogen/VDom/Attributes.purs index 804270b..45fecb0 100644 --- a/src/Halogen/VDom/Attributes.purs +++ b/src/Halogen/VDom/Attributes.purs @@ -1,10 +1,7 @@ module Halogen.VDom.Attributes where -import Prelude +import Prelude (Unit) -import Halogen.VDom.Util -import Halogen.VDom.DOM.Checkers -import Effect (Effect) import Web.DOM.Element as DOM import Effect.Uncurried as EFn import Unsafe.Coerce (unsafeCoerce) @@ -15,8 +12,7 @@ data NamedNodeMap foreign import attributes ∷ DOM.Element → NamedNodeMap forEachE - ∷ ∀ a - . EFn.EffectFn2 + ∷ EFn.EffectFn2 NamedNodeMap (EFn.EffectFn1 String Unit) Unit diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 6b7dfbb..16aa38f 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -4,36 +4,20 @@ module Halogen.VDom.DOM , hydrateVDom ) where -import Halogen.VDom.DOM.Elem -import Halogen.VDom.DOM.Keyed -import Halogen.VDom.DOM.Text -import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Widget -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 Data.Array as Array -import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) -import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) -import Debug.Trace (traceM) import Effect.Uncurried as EFn -import Foreign.Object as Object import Halogen.VDom.DOM.Elem (buildElem) as Export import Halogen.VDom.DOM.Keyed (buildKeyed) as Export import Halogen.VDom.DOM.Text (buildText) as Export import Halogen.VDom.DOM.Types (VDomSpec(..)) as Export -import Halogen.VDom.DOM.Checkers import Halogen.VDom.DOM.Widget (buildWidget) as Export -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) -import Halogen.VDom.Util as Util -import Halogen.VDom.Util -import Web.DOM.Document (Document) as DOM +import Halogen.VDom.Types (VDom(..), runGraft) import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOM.Element -import Web.DOM.Node (Node) as DOM -- | Starts an initial `VDom` machine by providing a `VDomSpec`. -- | diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 591b448..92d9274 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -1,42 +1,36 @@ module Halogen.VDom.DOM.Checkers where -import Prelude +import Prelude (Unit, bind, discard, show, when, ($), (/=), (<>)) -import Data.Maybe (Maybe(..)) -import Data.Newtype (unwrap) -import Data.Nullable (Nullable, toMaybe, toNullable) +import Data.Maybe (Maybe) import Data.String (toUpper) -import Data.Tuple (Tuple(..), fst) import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Types (ElemName, Namespace) import Halogen.VDom.Util import Partial.Unsafe (unsafePartial) -import Unsafe.Coerce (unsafeCoerce) -import Web.DOM as DOM -import Web.DOM.Document as DOM -import Web.DOM.Element as DOM +import Web.DOM (NodeList, NodeType) as DOM +import Web.DOM.Element (Element, tagName) as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.HTMLCollection (length) as DOM.HTMLCollection -import Web.DOM.Node as DOM +import Web.DOM.Node (childNodes, nodeType, textContent) as DOM import Web.DOM.NodeList (length) as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType -import Web.DOM.ParentNode (children) as DOM.ParentNode -------------------------------------- -- Text -getElementNodeType :: DOM.Element -> DOM.NodeType -getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode element) checkElementIsNodeType :: DOM.NodeType -> DOM.Element -> Effect Unit -checkElementIsNodeType expectedNodeType element = - let nodeType = getElementNodeType element - in when (nodeType /= expectedNodeType) (throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) +checkElementIsNodeType = checkElementIsNodeType' + where + getElementNodeType :: DOM.Element -> DOM.NodeType + getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode element) + + checkElementIsNodeType' :: DOM.NodeType -> DOM.Element -> Effect Unit + checkElementIsNodeType' expectedNodeType element = + let nodeType = getElementNodeType element + in when (nodeType /= expectedNodeType) (throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) checkIsTextNode :: DOM.Element -> Effect Unit checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 7cd99df..4a28c1c 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -1,34 +1,23 @@ module Halogen.VDom.DOM.Elem where -import Data.Tuple.Nested -import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Checkers +import Data.Tuple.Nested (type (/\), (/\)) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) import Prelude import Data.Array (length, zip) as Array -import Data.Array as Array import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) -import Debug.Trace (traceM) -import Effect (Effect) -import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) +import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) import Halogen.VDom.Util as Util import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document as DOM -import Web.DOM.Element as DOM +import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.HTMLCollection (toArray) as DOM.HTMLCollection -import Web.DOM.Node as DOM +import Web.DOM.Node (Node, childNodes) as DOM import Web.DOM.NodeList as DOM.NodeList -import Web.DOM.ParentNode as DOM.ParentNode -import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) type ElemState a w = { build ∷ VDomMachine a w diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 6500dfa..996c2e1 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -1,28 +1,25 @@ module Halogen.VDom.DOM.Keyed where -import Prelude +import Prelude (Unit, bind, discard, pure, ($), (>>=)) import Data.Array as Array import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst) import Effect.Uncurried as EFn import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) +import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) +import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) import Halogen.VDom.Util as Util -import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element import Web.DOM.NodeList as DOM.NodeList import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Node as DOM -import Halogen.VDom.DOM.Types -import Halogen.VDom.DOM.Checkers -import Halogen.VDom.DOM.Prop (hydrateProp, buildProp) -import Data.Tuple.Nested +import Web.DOM.Node (Node, childNodes) as DOM +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) type KeyedState a w = { build ∷ VDomMachine a w diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 2481369..5576f2b 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -6,7 +6,7 @@ module Halogen.VDom.DOM.Prop import Data.String.Common (joinWith) import Halogen.VDom.DOM.Prop.Implementation (applyProp, diffProp, hydrateApplyProp, mbEmit, removeProp) -import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropState) +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, (#), ($), (<>), (>)) @@ -19,11 +19,10 @@ 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, Machine) +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 -import Halogen.VDom.DOM.Prop.Types (BuildPropFunction) -- inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030 mkExtraAttributeNames ∷ DOM.Element → Effect (Set.Set String) diff --git a/src/Halogen/VDom/DOM/Prop/Types.purs b/src/Halogen/VDom/DOM/Prop/Types.purs index af4b977..8612212 100644 --- a/src/Halogen/VDom/DOM/Prop/Types.purs +++ b/src/Halogen/VDom/DOM/Prop/Types.purs @@ -2,21 +2,17 @@ module Halogen.VDom.DOM.Prop.Types where import Prelude -import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) -import Data.Nullable (null, toNullable, Nullable) -import Data.Tuple (Tuple(..), fst, snd) +import Data.Maybe (Maybe) +import Data.Tuple (Tuple) import Effect (Effect) import Effect.Ref as Ref -import Effect.Uncurried as EFn -import Foreign (typeOf) import Foreign.Object as Object -import Halogen.VDom.Machine (Step, Step'(..), mkStep, Machine) -import Halogen.VDom.Types (Namespace(..)) +import Halogen.VDom.Machine (Machine) +import Halogen.VDom.Types (Namespace) import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM -import Web.Event.Event (EventType(..), Event) as DOM -import Web.Event.EventTarget (eventListener, EventListener) as DOM +import Web.Event.Event (Event, EventType) as DOM +import Web.Event.EventTarget (EventListener) as DOM -- | Attributes, properties, event handlers, and element lifecycles. -- | Parameterized by the type of handlers outputs. diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index 282da12..b07b8f5 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -2,22 +2,11 @@ module Halogen.VDom.DOM.Types where import Prelude -import Data.Array as Array -import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe(..)) -import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) -import Effect (Effect) import Effect.Uncurried as EFn -import Foreign.Object as Object -import Halogen.VDom.Machine (Machine, Step, Step'(..), extract, halt, mkStep, step, unStep) -import Halogen.VDom.Machine as Machine -import Halogen.VDom.Types (ElemName(..), Namespace(..), VDom(..), runGraft) -import Halogen.VDom.Util as Util -import Halogen.VDom.DOM.Prop.Types (BuildPropFunction, Prop) +import Halogen.VDom.Machine (Machine, Step) +import Halogen.VDom.Types (VDom) import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM -- A function, that takes `VDom a w` and builds a `DOM.Node` diff --git a/src/Halogen/VDom/Set.purs b/src/Halogen/VDom/Set.purs index 8ae850e..aded579 100644 --- a/src/Halogen/VDom/Set.purs +++ b/src/Halogen/VDom/Set.purs @@ -1,8 +1,6 @@ module Halogen.VDom.Set where -import Halogen.VDom.DOM.Checkers -import Halogen.VDom.Util -import Prelude +import Prelude (Unit) import Effect (Effect) import Effect.Uncurried (EffectFn2) as EFn diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 69676cb..8b3c52d 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -21,10 +21,7 @@ import Halogen.VDom.Util as Util import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Node (Node) import Halogen.VDom.DOM.Types (VDomMachine) -import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM -import Web.DOM.Element as DOM.Element -import Web.DOM.Node (Node) as DOM foreign import data ThunkArg ∷ Type @@ -109,7 +106,7 @@ hydrateThunk → V.VDomSpec a w → DOM.Element → V.Machine (Thunk f i) Node -hydrateThunk toVDom spec element = mkThunkBuilder (\spec → V.hydrateVDom spec element) toVDom spec +hydrateThunk toVDom spec element = mkThunkBuilder (\spec' → V.hydrateVDom spec' element) toVDom spec buildThunk ∷ ∀ f i a w diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index 12e8a80..a5c4fca 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -176,26 +176,6 @@ exports.removeEventListener = function (ev, listener, el) { exports.jsUndefined = void 0; -exports.getNodeType = function(el) { - return el.nodeType -} - -exports.nodeIsTextNode = function(el) { - return el.nodeType === 3 -} - -exports.nodeIsElementNode = function(el) { - return el.nodeType === 1 -} - -exports.getTextContent = function(el) { - return node.textContent; -} - -exports.getNamespaceURI = function(el) { - return node.namespaceURI -} - exports.anyToString = function (a) { return String(a); }; diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index b1ca1c2..511ffdd 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -1,40 +1,6 @@ module Halogen.VDom.Util where - -- | ( newMutMap - -- | , pokeMutMap - -- | , deleteMutMap - -- | , unsafeFreeze - -- | , unsafeLookup - -- | , unsafeGetAny - -- | , unsafeHasAny - -- | , unsafeSetAny - -- | , unsafeDeleteAny - -- | , forE - -- | , forEachE - -- | , forInE - -- | , replicateE - -- | , diffWithIxE - -- | , diffWithKeyAndIxE - -- | , strMapWithIxE - -- | , refEq - -- | , createTextNode - -- | , setTextContent - -- | , createElement - -- | , insertChildIx - -- | , removeChild - -- | , parentNode - -- | , setAttribute - -- | , removeAttribute - -- | , hasAttribute - -- | , addEventListener - -- | , removeEventListener - -- | , JsUndefined - -- | , jsUndefined - -- | , STObject' - -- | , nodeIsTextNode - -- | , nodeIsElementNode - -- | ) where - -import Prelude + +import Prelude (Unit, unit, (<>), (==)) import Data.Function.Uncurried as Fn import Data.Nullable (Nullable) @@ -42,9 +8,8 @@ import Effect (Effect) import Effect.Uncurried as EFn import Foreign.Object (Object) import Foreign.Object as Object -import Foreign.Object.ST (STObject) import Foreign.Object.ST as STObject -import Halogen.VDom.Types +import Halogen.VDom.Types (ElemName(..), Namespace(..)) import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM diff --git a/test/Hydration.purs b/test/Hydration.purs index 63659d0..6f69faa 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -2,29 +2,21 @@ module Test.Hydration where import Prelude -import Control.Alternative (void) -import Data.Foldable (for_, traverse_) -import Data.Maybe (Maybe(..), isNothing, maybe) -import Data.Newtype (un, wrap) +import Data.Maybe (maybe) +import Data.Newtype (un) import Data.Tuple (Tuple(..)) import Effect (Effect) import Effect.Exception (error, throwException) -import Effect.Ref as Ref -import Effect.Timer as Timer import Effect.Uncurried as EFn import Halogen.VDom as V -import Halogen.VDom.DOM.Prop (Prop) -import Halogen.VDom.Thunk (Thunk) import Halogen.VDom.Util (addEventListener) as Util import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=)) import Web.DOM.Element (Element) -import Web.DOM.Element (toNode) as DOM import Web.DOM.Element (toParentNode) as DOM.Element -import Web.DOM.Node (Node, appendChild) as DOM import Web.DOM.ParentNode (ParentNode) import Web.DOM.ParentNode (firstElementChild) as DOM.ParentNode import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM -import Web.Event.EventTarget (eventListener, EventListener) as DOM +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 diff --git a/test/TestVdom.purs b/test/TestVdom.purs index 471886f..f2d1dac 100644 --- a/test/TestVdom.purs +++ b/test/TestVdom.purs @@ -9,10 +9,9 @@ import Data.Newtype (class Newtype, un) import Data.Tuple (Tuple) import Halogen.VDom as V import Halogen.VDom.DOM.Prop (Prop(..), propFromString, buildProp, hydrateProp) -import Halogen.VDom.DOM.Prop.Types (BuildPropFunction) import Halogen.VDom.Thunk (Thunk, thunk1, buildThunk, hydrateThunk) import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Document as DOM +import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Halogen.VDom.Machine (Machine) import Effect (Effect) From c17301b531100e1d55e6c7c73516b9e1ef49c563 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 3 Jun 2020 15:31:01 +0300 Subject: [PATCH 17/48] revert(#b385b50) --- GUIDE.md | 6 +-- src/Halogen/VDom/DOM.purs | 6 +-- src/Halogen/VDom/DOM/Elem.purs | 2 +- src/Halogen/VDom/DOM/Prop.purs | 15 ------- src/Halogen/VDom/DOM/Prop/Types.purs | 4 +- src/Halogen/VDom/DOM/Prop/Utils.purs | 6 +-- src/Halogen/VDom/DOM/Types.purs | 9 ---- src/Halogen/VDom/Machine.purs | 19 --------- src/Halogen/VDom/Thunk.purs | 1 - src/Halogen/VDom/Util.js | 61 ++++++++++++++-------------- src/Halogen/VDom/Util.purs | 48 ++++++++++------------ 11 files changed, 62 insertions(+), 115 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 245cd12..2dfb19e 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -14,14 +14,12 @@ render ∷ MyState → MyVDom main = do -- Build the initial machine - (machine1 :: VDomMachine a w) ← V.buildVDom myVDomSpec (render state1) + machine1 ← V.buildVDom myVDomSpec (render state1) - -- `machine1` contains a new `DOM.Node` (output node) in it's state - -- Attach that output node to the DOM + -- Attach the output node to the DOM appendChildToBody (V.extract machine1) -- Patch - -- `V.step` patches previous `DOM.Node` (stored in `machine1`) by running effects machine2 ← V.step machine1 (render state2) machine3 ← V.step machine2 (render state3) ... diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 16aa38f..28ef38b 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -44,8 +44,8 @@ buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where build = EFn.mkEffectFn1 case _ of - Text s → EFn.runEffectFn3 buildText spec build s -- build text machine + Text s → EFn.runEffectFn3 buildText spec build s Elem namespace elemName a childrenVdoms → EFn.runEffectFn6 buildElem spec build namespace elemName a childrenVdoms Keyed namespace elemName a keyedChildrenVdoms → EFn.runEffectFn6 buildKeyed spec build namespace elemName a keyedChildrenVdoms - Widget w → EFn.runEffectFn3 buildWidget spec build w -- machine that has full control of it's lifecycle - Grafted g → EFn.runEffectFn1 build (runGraft g) -- optimization + Widget w → EFn.runEffectFn3 buildWidget spec build w + Grafted g → EFn.runEffectFn1 build (runGraft g) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 4a28c1c..b3684b6 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -88,7 +88,7 @@ buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res children ← EFn.runEffectFn2 Util.forE ch1 onChild - attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 -- build machine that takes attributes + attrs ← EFn.runEffectFn1 (spec.buildAttributes el) as1 let state = { build diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 5576f2b..7b1422a 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -52,11 +52,6 @@ hydrateProp emit el = renderProp extraAttributeNames ← mkExtraAttributeNames el - -- for each prop in array: - -- if prop is attr - dont set attr to element, store attr under "attr/XXX" key in a returned object - -- if prop is property - dont set property to element, store property under "prop/XXX" key in a returned object - -- if prop is handler for DOM.EventType - start listen and add listener to `events` mutable map, store handler under "handler/EVENTTYPE" in a returned object - -- if prop is ref updater - store `emitterInputBuilder` in under a `ref` key in a returned object, call `emitter` on creation of all props (now) and on halt of all props (later) (props ∷ Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn4 hydrateApplyProp extraAttributeNames el emit events) let (state ∷ PropState a) = @@ -75,19 +70,9 @@ buildProp . BuildPropFunction a buildProp emit el = renderProp where - -- what it does - creates a machine, that contains state - -- on next step - patches prop - -- on halt - all ref watchers are notified that element is removed - renderProp ∷ EFn.EffectFn1 (Array (Prop a)) (Step (Array (Prop a)) Unit) renderProp = EFn.mkEffectFn1 \ps1 → do (events ∷ STObject' (EventListenerAndCurrentEmitterInputBuilder a)) ← Util.newMutMap - - -- for each prop in array: - -- if prop is attr - set attr to element, store attr under "attr/XXX" key in a returned object - -- if prop is property - set property to element, store property under "prop/XXX" key in a returned object - -- if prop is handler for DOM.EventType - start listen and add listener to `events` mutable map, store handler under "handler/EVENTTYPE" in a returned object - -- if prop is ref updater - store `emitterInputBuilder` in under a `ref` key in a returned object, call `emitter` on creation of all props (now) and on halt of all props (later) (props ∷ Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn3 applyProp el emit events) let (state ∷ PropState a) = diff --git a/src/Halogen/VDom/DOM/Prop/Types.purs b/src/Halogen/VDom/DOM/Prop/Types.purs index 8612212..f0d9161 100644 --- a/src/Halogen/VDom/DOM/Prop/Types.purs +++ b/src/Halogen/VDom/DOM/Prop/Types.purs @@ -88,6 +88,6 @@ type PropState a = } type BuildPropFunction a - = (a → Effect Unit) -- emitter, for example the global broadcaster function for all elements in halogen component + = (a → Effect Unit) -- Emitter, for example the global broadcaster function for all elements in halogen component → DOM.Element - → Machine (Array (Prop a)) Unit -- Machine takes array of properties for that element, outputs nothing + → Machine (Array (Prop a)) Unit diff --git a/src/Halogen/VDom/DOM/Prop/Utils.purs b/src/Halogen/VDom/DOM/Prop/Utils.purs index 75e9d2c..9fad7dc 100644 --- a/src/Halogen/VDom/DOM/Prop/Utils.purs +++ b/src/Halogen/VDom/DOM/Prop/Utils.purs @@ -29,9 +29,9 @@ unsafeGetProperty = Util.unsafeGetAny removeProperty ∷ EFn.EffectFn2 String DOM.Element Unit removeProperty = EFn.mkEffectFn2 \key el → - EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ -- If attr exists on element - then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el -- remove it using el.removeAttribute() - else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of -- If it's property - set to following + EFn.runEffectFn3 Util.hasAttribute (null ∷ Nullable Namespace) key el >>= if _ + then EFn.runEffectFn3 Util.removeAttribute (null ∷ Nullable Namespace) key el + else case typeOf (Fn.runFn2 Util.unsafeGetAny key el) of "string" → EFn.runEffectFn3 Util.unsafeSetAny key "" el _ → case key of "rowSpan" → EFn.runEffectFn3 Util.unsafeSetAny key 1 el diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index b07b8f5..0367976 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -9,15 +9,12 @@ import Web.DOM.Document (Document) as DOM import Web.DOM.Element (Element) as DOM import Web.DOM.Node (Node) as DOM --- A function, that takes `VDom a w` and builds a `DOM.Node` type VDomMachine a w = Machine (VDom a w) DOM.Node type VDomStep a w = Step (VDom a w) DOM.Node type VDomInit i a w = EFn.EffectFn1 i (VDomStep a w) --- Equal to --- (VDomSpec a w) -> (VDOM a w -> Step (VDOM a w) DOM.Node) -> i -> Effect (Step (VDOM a w) DOM.Node) type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomStep a w) type VDomHydrator i a w @@ -48,13 +45,7 @@ type VDomHydrator4 i j k l a w newtype VDomSpec a w = VDomSpec { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` , hydrateWidget ∷ VDomSpec a w → DOM.Element → Machine w DOM.Node - -- example: - -- buildAttributes = buildProps handler - -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/VDom/Driver.purs#L68-L71 - - -- what is handler - -- https://github.com/purescript-halogen/purescript-halogen/blob/bb715fe5c06ba3048f4d8b377ec842cd8cf37833/src/Halogen/Aff/Driver.purs#L203 , buildAttributes ∷ DOM.Element → Machine a Unit , hydrateAttributes ∷ DOM.Element → Machine a Unit diff --git a/src/Halogen/VDom/Machine.purs b/src/Halogen/VDom/Machine.purs index 469b18d..072ff2a 100644 --- a/src/Halogen/VDom/Machine.purs +++ b/src/Halogen/VDom/Machine.purs @@ -14,31 +14,12 @@ import Prelude import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, mkEffectFn2, runEffectFn1, runEffectFn2) import Unsafe.Coerce (unsafeCoerce) -{- - -type Machine is equal to: - -a -> Step a b -a -> forall state . Step b state (state -> a -> Step a b) (state -> Unit) -a -> forall state . Step b state (state -> Machine a b) (state -> Unit) - -where - -a is input -b is output -state is hidden state -(state -> a -> Step a b) is a functon from state and input to the new Step -(state -> Unit) is finalizer - --} - type Machine a b = EffectFn1 a (Step a b) data Step' a b s = Step b s (EffectFn2 s a (Step a b)) (EffectFn1 s Unit) foreign import data Step ∷ Type → Type → Type --- hides state type, makes it exsistential mkStep ∷ ∀ a b s. Step' a b s → Step a b mkStep = unsafeCoerce diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 8b3c52d..d9b615d 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -27,7 +27,6 @@ foreign import data ThunkArg ∷ Type foreign import data ThunkId ∷ Type ---- widget type can be a thunk data Thunk f i = Thunk ThunkId diff --git a/src/Halogen/VDom/Util.js b/src/Halogen/VDom/Util.js index a5c4fca..8a1627a 100644 --- a/src/Halogen/VDom/Util.js +++ b/src/Halogen/VDom/Util.js @@ -44,26 +44,36 @@ exports.replicateE = function (n, f) { } }; -exports.diffWithIxE = function (oldElems, newElems, onBothElements, onOldElement, onNewElement) { - var outputs = []; - var oldElemsLength = oldElems.length; - var newElemsLength = newElems.length; +exports.diffWithIxE = function (a1, a2, f1, f2, f3) { + var a3 = []; + var l1 = a1.length; + var l2 = a2.length; var i = 0; while (1) { - if (i < oldElemsLength) { - if (i < newElemsLength) { - outputs.push(onBothElements(i, oldElems[i], newElems[i])); + if (i < l1) { + if (i < l2) { + a3.push(f1(i, a1[i], a2[i])); } else { - onOldElement(i, oldElems[i]); + f2(i, a1[i]); } - } else if (i < newElemsLength) { - outputs.push(onNewElement(i, newElems[i])); + } else if (i < l2) { + a3.push(f3(i, a2[i])); } else { break; } i++; } - return outputs; + return a3; +}; + +exports.strMapWithIxE = function (as, fk, f) { + var o = {}; + for (var i = 0; i < as.length; i++) { + var a = as[i]; + var k = fk(a); + o[k] = f(k, i, a); + } + return o; }; exports.diffWithKeyAndIxE = function (o1, as, fk, f1, f2, f3) { @@ -86,16 +96,6 @@ exports.diffWithKeyAndIxE = function (o1, as, fk, f1, f2, f3) { return o2; }; -exports.strMapWithIxE = function (children, propToStrKey, f) { - var o = {}; - for (var i = 0; i < children.length; i++) { - var child = children[i]; - var key = propToStrKey(child); - o[key] = f(key, i, child); - } - return o; -}; - exports.refEq = function (a, b) { return a === b; }; @@ -116,22 +116,21 @@ exports.createElement = function (ns, name, doc) { } }; -exports.insertChildIx = function (i, elem, parent) { - var referenceNode = parent.childNodes.item(i) || null; - if (referenceNode !== elem) { - // insert before referenceNode, if referenceNode is null - inserted at the end - parent.insertBefore(elem, referenceNode); +exports.insertChildIx = function (i, a, b) { + var n = b.childNodes.item(i) || null; + if (n !== a) { + b.insertBefore(a, n); } }; -exports.removeChild = function (elem, parent) { - if (parent && elem.parentNode === parent) { - parent.removeChild(elem); +exports.removeChild = function (a, b) { + if (b && a.parentNode === b) { + b.removeChild(a); } }; -exports.parentNode = function (elem) { - return elem.parentNode; +exports.parentNode = function (a) { + return a.parentNode; }; exports.setAttribute = function (ns, attr, val, el) { diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 511ffdd..f7a21d5 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -1,6 +1,6 @@ module Halogen.VDom.Util where -import Prelude (Unit, unit, (<>), (==)) +import Prelude (Unit, (<>), (==)) import Data.Function.Uncurried as Fn import Data.Nullable (Nullable) @@ -75,33 +75,33 @@ foreign import replicateE Unit foreign import diffWithIxE - ∷ ∀ oldElem newElem output dismissed + ∷ ∀ b c d . EFn.EffectFn5 - (Array oldElem) -- e.g. list of vdom elements - (Array newElem) -- e.g. list of vdom elements - (EFn.EffectFn3 Int oldElem newElem output) -- execute action when both elems are found in oldElems array and newElems array under the same index (usually used to remove old element from DOM and add new element to DOM) - (EFn.EffectFn2 Int oldElem dismissed) -- execute action when only oldElem is found, there are no elems left in `Array newElem` (happens when array of old elements is bigger than array of new elements) - (EFn.EffectFn2 Int newElem output) -- execute action when only newElem is found, there are no elems left in `Array oldElem` (happens when array of new elements is bigger than array of old elements) - (Array output) -- e.g. list of dom elements + (Array b) + (Array c) + (EFn.EffectFn3 Int b c d) + (EFn.EffectFn2 Int b Unit) + (EFn.EffectFn2 Int c d) + (Array d) foreign import diffWithKeyAndIxE - ∷ ∀ oldElem newElemWithKey output dismissed + ∷ ∀ a b c d . EFn.EffectFn6 - (Object.Object oldElem) - (Array newElemWithKey) - (newElemWithKey → String) - (EFn.EffectFn4 String Int oldElem newElemWithKey output) - (EFn.EffectFn2 String oldElem dismissed) - (EFn.EffectFn3 String Int newElemWithKey output) - (Object.Object output) + (Object.Object a) + (Array b) + (b → String) + (EFn.EffectFn4 String Int a b c) + (EFn.EffectFn2 String a d) + (EFn.EffectFn3 String Int b c) + (Object.Object c) foreign import strMapWithIxE - ∷ ∀ child outputVal + ∷ ∀ a b . EFn.EffectFn3 - (Array child) -- children - (child → String) -- propToStrKey - (EFn.EffectFn3 String Int child outputVal) -- action, executed on each array element, (StrKey -> Index -> child -> outputVal) - (Object.Object outputVal) -- key is StrKey, val type is outputVal + (Array a) + (a → String) + (EFn.EffectFn3 String Int a b) + (Object.Object b) foreign import refEq ∷ ∀ a b. Fn.Fn2 a b Boolean @@ -115,9 +115,6 @@ foreign import setTextContent foreign import createElement ∷ EFn.EffectFn3 (Nullable Namespace) ElemName DOM.Document DOM.Element --- Insert new child at index --- (if there is already an element on that index, that old element is moved below). --- If there are not enough elements - new child is moved at the end of the list. foreign import insertChildIx ∷ EFn.EffectFn3 Int DOM.Node DOM.Node Unit @@ -153,9 +150,6 @@ foreign import warnAny ∷ ∀ a . EFn.EffectFn2 String a Unit foreign import logAny ∷ ∀ a . EFn.EffectFn2 String a Unit -undefined :: ∀ a . a -undefined = unsafeCoerce unit - fullAttributeName ∷ Maybe Namespace → ElemName → String fullAttributeName maybeNamespace elemName = case maybeNamespace of From 67221527a4389e9c1b403c23714720c502d48b82 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 3 Jun 2020 15:44:01 +0300 Subject: [PATCH 18/48] refactor: thunk --- src/Halogen/VDom/Thunk.purs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index d9b615d..abc149d 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -105,26 +105,25 @@ hydrateThunk → V.VDomSpec a w → DOM.Element → V.Machine (Thunk f i) Node -hydrateThunk toVDom spec element = mkThunkBuilder (\spec' → V.hydrateVDom spec' element) toVDom spec +hydrateThunk toVDom spec element = mkThunkBuilder (V.hydrateVDom spec element) toVDom buildThunk ∷ ∀ f i a w . (f i → V.VDom a w) → V.VDomSpec a w → V.Machine (Thunk f i) Node -buildThunk = mkThunkBuilder V.buildVDom +buildThunk toVDom spec = mkThunkBuilder (V.buildVDom spec) toVDom mkThunkBuilder ∷ ∀ f i a w - . (V.VDomSpec a w → VDomMachine a w) + . VDomMachine a w → (f i → V.VDom a w) - → V.VDomSpec a w → V.Machine (Thunk f i) Node -mkThunkBuilder buildVDom toVDom = renderThunk +mkThunkBuilder build toVDom = renderThunk where - renderThunk ∷ V.VDomSpec a w → V.Machine (Thunk f i) Node - renderThunk spec = EFn.mkEffectFn1 \t → do - vdom ← EFn.runEffectFn1 (buildVDom spec) (toVDom (runThunk t)) + renderThunk ∷ V.Machine (Thunk f i) Node + renderThunk = EFn.mkEffectFn1 \t → do + vdom ← EFn.runEffectFn1 build (toVDom (runThunk t)) pure $ M.mkStep $ M.Step (M.extract vdom) { thunk: t, vdom } patchThunk haltThunk patchThunk ∷ EFn.EffectFn2 (ThunkState f i a w) (Thunk f i) (V.Step (Thunk f i) Node) From d08547124761b58b946f885071cff1ceeba3f744 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 3 Jun 2020 15:44:14 +0300 Subject: [PATCH 19/48] refactor: move comment where it belongs --- src/Halogen/VDom/DOM.purs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 28ef38b..3a6798f 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -19,15 +19,6 @@ import Halogen.VDom.DOM.Widget (buildWidget) as Export import Halogen.VDom.Types (VDom(..), runGraft) import Web.DOM.Element (Element) as DOM --- | Starts an initial `VDom` machine by providing a `VDomSpec`. --- | --- | ```purescript --- | main = do --- | machine1 ← buildVDom spec vdomTree1 --- | machine2 ← Machine.step machine1 vdomTree2 --- | machine3 ← Machine.step machine2 vdomTree3 --- | ... --- | ```` hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Element -> VDomMachine a w hydrateVDom spec rootNode = hydrate rootNode where @@ -40,6 +31,15 @@ hydrateVDom spec rootNode = hydrate rootNode Widget w → EFn.runEffectFn5 hydrateWidget node spec hydrate build w Grafted g → EFn.runEffectFn1 (hydrate node) (runGraft g) +-- | Starts an initial `VDom` machine by providing a `VDomSpec`. +-- | +-- | ```purescript +-- | main = do +-- | machine1 ← buildVDom spec vdomTree1 +-- | machine2 ← Machine.step machine1 vdomTree2 +-- | machine3 ← Machine.step machine2 vdomTree3 +-- | ... +-- | ```` buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where From 3fb49b1bd30fc8d8b094f8f07b29823e36f7e09a Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 4 Jun 2020 13:28:06 +0300 Subject: [PATCH 20/48] feat: create Halogen.VDom.Finders --- src/Halogen/VDom/DOM/Checkers.purs | 14 +++++++++---- src/Halogen/VDom/DOM/Prop/Checkers.purs | 17 ++++++++++------ src/Halogen/VDom/Finders.purs | 26 +++++++++++++++++++++++++ test/Hydration.purs | 16 ++------------- 4 files changed, 49 insertions(+), 24 deletions(-) create mode 100644 src/Halogen/VDom/Finders.purs diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 92d9274..db3a178 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -8,7 +8,7 @@ import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Halogen.VDom.Types (ElemName, Namespace) -import Halogen.VDom.Util +import Halogen.VDom.Util (fullAttributeName, quote, warnAny) import Partial.Unsafe (unsafePartial) import Web.DOM (NodeList, NodeType) as DOM import Web.DOM.Element (Element, tagName) as DOM @@ -30,7 +30,9 @@ checkElementIsNodeType = checkElementIsNodeType' checkElementIsNodeType' :: DOM.NodeType -> DOM.Element -> Effect Unit checkElementIsNodeType' expectedNodeType element = let nodeType = getElementNodeType element - in when (nodeType /= expectedNodeType) (throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) + in when (nodeType /= expectedNodeType) (do + EFn.runEffectFn2 warnAny "Error at " { element } + throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) checkIsTextNode :: DOM.Element -> Effect Unit checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode @@ -38,7 +40,9 @@ checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode checkTextContentIsEqTo :: String -> DOM.Element -> Effect Unit checkTextContentIsEqTo expectedText element = do textContent <- DOM.textContent (DOM.Element.toNode element) - when (textContent /= expectedText) (throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) + when (textContent /= expectedText) (do + EFn.runEffectFn2 warnAny "Error at " { element } + throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) -------------------------------------- -- Elem @@ -53,7 +57,9 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do expectedTagName :: String expectedTagName = toUpper $ fullAttributeName maybeNamespace elemName let tagName = DOM.tagName element - when (tagName /= expectedTagName) (throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) + when (tagName /= expectedTagName) (do + EFn.runEffectFn2 warnAny "Error at " { element } + throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 38f5c1a..696c1f6 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -2,7 +2,7 @@ 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, ($), (<#>), (<>), (==)) +import Prelude (Unit, bind, pure, unit, ($), (<#>), (<>), (==), discard) import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) @@ -10,7 +10,7 @@ import Data.Nullable (toMaybe, toNullable) import Effect (Effect) import Effect.Uncurried as EFn import Halogen.VDom.Types (ElemName(..), Namespace) -import Halogen.VDom.Util (anyToString, fullAttributeName, quote) +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) @@ -19,16 +19,21 @@ checkAttributeExistsAndIsEqual ∷ Maybe Namespace → String → String → DOM checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue element = do elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe case elementValue of - Nothing → throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it is missing" + Nothing → 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 is missing" Just elementValue' → if elementValue' == expectedElementValue then pure unit - else throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it was equal to " <> quote elementValue' + 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' checkPropExistsAndIsEqual ∷ String → PropValue → DOM.Element → Effect Unit -checkPropExistsAndIsEqual propName expectedPropValue el = do - let propValue = Fn.runFn2 unsafeGetProperty propName el +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) diff --git a/src/Halogen/VDom/Finders.purs b/src/Halogen/VDom/Finders.purs new file mode 100644 index 0000000..d72f0a2 --- /dev/null +++ b/src/Halogen/VDom/Finders.purs @@ -0,0 +1,26 @@ +module Halogen.VDom.Finders where + +import Prelude +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 (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 + +-- | Used for hydration +findRootElementInsideOfRootContainer :: Element -> Effect Element +findRootElementInsideOfRootContainer container = do + 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 diff --git a/test/Hydration.purs b/test/Hydration.purs index 6f69faa..1d72b3b 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -2,24 +2,18 @@ module Test.Hydration where import Prelude -import Data.Maybe (maybe) import Data.Newtype (un) import Data.Tuple (Tuple(..)) import Effect (Effect) -import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Halogen.VDom as V import Halogen.VDom.Util (addEventListener) as Util import Test.TestVdom (VDom(..), elem, keyed, mkSpec, text, thunk, (:=)) -import Web.DOM.Element (Element) -import Web.DOM.Element (toParentNode) as DOM.Element -import Web.DOM.ParentNode (ParentNode) -import Web.DOM.ParentNode (firstElementChild) as DOM.ParentNode -import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM 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 } @@ -46,19 +40,13 @@ renderData stateArray = [ "className" := elementState.classes ] [ text elementState.text ] -findRequiredElement ∷ String → ParentNode → Effect Element -findRequiredElement selector parentNode = - DOM.querySelector (DOM.QuerySelector selector) parentNode - >>= maybe (throwException (error $ selector <> " not found")) pure - main ∷ Effect Unit main = do win ← DOM.window doc ← DOM.document win appDiv ← findRequiredElement "#app" (DOM.toParentNode doc) - rootElement ← (appDiv # DOM.Element.toParentNode # DOM.ParentNode.firstElementChild) - >>= maybe (throwException (error $ "rootElement not found")) pure + rootElement ← findRootElementInsideOfRootContainer appDiv updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc) From b98e64869fa1218e162f8a2f1bed5ecc2aefc125 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 4 Jun 2020 17:49:48 +0300 Subject: [PATCH 21/48] feat: check extra attributes --- src/Halogen/VDom/Attributes.purs | 2 +- src/Halogen/VDom/DOM.purs | 3 ++ src/Halogen/VDom/DOM/Prop.purs | 29 ++---------- src/Halogen/VDom/DOM/Prop/Checkers.purs | 46 +++++++++++++------ src/Halogen/VDom/DOM/Prop/Implementation.purs | 28 +++++++---- src/Halogen/VDom/Finders.purs | 33 ++++++------- test/Hydration.purs | 16 ++++++- 7 files changed, 88 insertions(+), 69 deletions(-) diff --git a/src/Halogen/VDom/Attributes.purs b/src/Halogen/VDom/Attributes.purs index 45fecb0..a9f86f3 100644 --- a/src/Halogen/VDom/Attributes.purs +++ b/src/Halogen/VDom/Attributes.purs @@ -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 diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 3a6798f..f462ac9 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -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 @@ -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 diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 7b1422a..c6485e9 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -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 @@ -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 diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 696c1f6..9b8b043 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -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 @@ -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 ", ") + ) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 58d83f3..ebe73d1 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -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 @@ -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 diff --git a/src/Halogen/VDom/Finders.purs b/src/Halogen/VDom/Finders.purs index d72f0a2..5e9176e 100644 --- a/src/Halogen/VDom/Finders.purs +++ b/src/Halogen/VDom/Finders.purs @@ -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 diff --git a/test/Hydration.purs b/test/Hydration.purs index 1d72b3b..68734e4 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -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 } @@ -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) From 6a9e94b6d2708c91d0b3d570baac6fa202bd3144 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 10 Jun 2020 20:30:35 +0300 Subject: [PATCH 22/48] feat: tracing --- src/Halogen/VDom/DOM/Checkers.purs | 15 ++++++++------- src/Halogen/VDom/DOM/Prop/Checkers.purs | 12 ++++++------ src/Halogen/VDom/DOM/Prop/Implementation.purs | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index db3a178..f0c588c 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -28,11 +28,12 @@ checkElementIsNodeType = checkElementIsNodeType' getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode element) checkElementIsNodeType' :: DOM.NodeType -> DOM.Element -> Effect Unit - checkElementIsNodeType' expectedNodeType element = + checkElementIsNodeType' expectedNodeType element = do let nodeType = getElementNodeType element - in when (nodeType /= expectedNodeType) (do - EFn.runEffectFn2 warnAny "Error at " { element } - throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) + EFn.runEffectFn2 warnAny "checkElementIsNodeType" { nodeType, expectedNodeType, meta: { element } } + when (nodeType /= expectedNodeType) (do + EFn.runEffectFn2 warnAny "Error at " { element } + throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) checkIsTextNode :: DOM.Element -> Effect Unit checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode @@ -40,8 +41,8 @@ checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode checkTextContentIsEqTo :: String -> DOM.Element -> Effect Unit checkTextContentIsEqTo expectedText element = do textContent <- DOM.textContent (DOM.Element.toNode element) + EFn.runEffectFn2 warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { element } } when (textContent /= expectedText) (do - EFn.runEffectFn2 warnAny "Error at " { element } throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) -------------------------------------- @@ -57,14 +58,14 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do expectedTagName :: String expectedTagName = toUpper $ fullAttributeName maybeNamespace elemName let tagName = DOM.tagName element + EFn.runEffectFn2 warnAny "checkTagNameIsEqualTo" { expectedTagName, tagName, meta: { maybeNamespace, elemName, element } } when (tagName /= expectedTagName) (do - EFn.runEffectFn2 warnAny "Error at " { element } throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) elementChildrenLength <- DOM.NodeList.length elementChildren + EFn.runEffectFn2 warnAny "checkChildrenLengthIsEqualTo" { elementChildrenLength, expectedLength, meta: { element, elementChildren } } when (elementChildrenLength /= expectedLength) do - EFn.runEffectFn2 warnAny "Error at " { element, elementChildren } (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 9b8b043..52e8624 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -24,19 +24,19 @@ checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe case elementValue of Nothing → do - EFn.runEffectFn2 warnAny "Error at " { element } + EFn.runEffectFn2 warnAny "checkAttributeExistsAndIsEqual -> missing" { element } throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it is missing" - Just elementValue' → + Just elementValue' → do + EFn.runEffectFn2 warnAny "checkAttributeExistsAndIsEqual -> not missing" { elementValue', expectedElementValue, meta: { maybeNamespace, attributeName, expectedElementValue, element } } 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 + EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propValue, expectedPropValue, meta: { element, propName } } 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) ) @@ -50,8 +50,8 @@ mkExtraAttributeNames el = do pure set checkExtraAttributeNamesIsEmpty ∷ Set.Set String -> DOM.Element -> Effect Unit -checkExtraAttributeNamesIsEmpty extraAttributeNames element = +checkExtraAttributeNamesIsEmpty extraAttributeNames element = do + EFn.runEffectFn2 warnAny "checkExtraAttributeNamesIsEmpty" { extraAttributeNames, meta: { element } } when (Set.size extraAttributeNames > 0) (do - EFn.runEffectFn2 warnAny "Error at " { element } throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") ) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index ebe73d1..8aaf5ef 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -14,7 +14,7 @@ import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListe 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, warnAny) +import Halogen.VDom.Util (STObject', fullAttributeName) import Halogen.VDom.Util as Util import Prelude (Unit, bind, discard, pure, unit, (==)) import Web.DOM.Element (Element) as DOM From 204e4c62ce647584ff2f9946692cb7e138cd92b1 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 12:49:26 +0300 Subject: [PATCH 23/48] feat: deleteRequiredElement --- HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md | 56 +++++++++++++++++++ src/Halogen/VDom/DOM/Prop/Implementation.purs | 20 +++++-- src/Halogen/VDom/Set.js | 4 ++ src/Halogen/VDom/Set.purs | 3 + test/Hydration.purs | 44 --------------- 5 files changed, 78 insertions(+), 49 deletions(-) create mode 100644 HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md diff --git a/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md b/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md new file mode 100644 index 0000000..0aa7733 --- /dev/null +++ b/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md @@ -0,0 +1,56 @@ +# how .attributes work + +`$0.attributes` are: +- is { required: "" } +- is { required: "false" } +- is { colspan: "1" }, but prop is colSpan = 1 +-
is { "foo:data-foo": "1" } + +# properties + +``` +Having `` +If do `$0.required = true` in chrome. + THE html `` + THE $0.attributes = { required: "true" } +``` + +``` +Having `` +If do `$0.required = false` in chrome. + THE html `` + THE $0.attributes = {} +``` + +# how .dataset property works + +``` +Having `
` + The `$0.attributes` + `NamedNodeMap { id: "1", data-foo: "foo", data-bar: "bar", data-baz-bak: "baz-bak", someint: "1" }` + The `$0.dataset` + `DOMStringMap { foo: "foo", bar: "bar", bazBak: "baz-bak", someint: "1" }` +``` + +Also, react doesnt support dataset property during hydration. Proof: + +- server = `
` +- client = `
` +- errorMessage = `Prop `dataset` did not match. Server: "null" Client: "[object Object]"` + +thus, react does support `data-***` attributes, but doesn't support `dataset` property, so why bother with supporting `dataset` property? + +But, in future, If we want to support dataset, then we sould implment something like + +```purs +data PropValue = PropValue_String String | PropValue_Int Int | ... | PropValue_Dataset (Object String) +removePropFromExtraAttributeNames ∷ PropName → PropValue → Set → Set +removePropFromExtraAttributeNames propName propValue set = + if propName == "dataset" + then forEach propValue + (\key _val → do + remove ("data-" <> camelCaseToKebabCase key) set + ) + else do + remove (camelCaseToKebabCase key) set +``` diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 8aaf5ef..55ce674 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -1,11 +1,15 @@ module Halogen.VDom.DOM.Prop.Implementation where +import Prelude + import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) +import Data.String (joinWith) import Data.String.Common (toLower) import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect) +import Effect.Exception (error, throwException) import Effect.Ref as Ref import Effect.Uncurried as EFn import Foreign.Object as Object @@ -14,13 +18,19 @@ import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListe 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, quote) 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 +deleteRequiredElement :: EFn.EffectFn2 String (Set.Set String) Unit +deleteRequiredElement = EFn.mkEffectFn2 \element set -> do + let isPresent = Fn.runFn2 Set.has element set + if isPresent + then EFn.runEffectFn2 Set.delete element set + else throwException $ error $ "Cannot delete element that is not present in set, " <> quote element <> " should be present in set" <> (Set.toArray set # joinWith ", ") + hydrateApplyProp ∷ ∀ a . Fn.Fn4 @@ -34,7 +44,7 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect Attribute maybeNamespace attributeName val → do checkAttributeExistsAndIsEqual maybeNamespace attributeName val el let fullAttributeName' = fullAttributeName maybeNamespace (ElemName attributeName) -- should be lowercased - EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames + EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames pure v Property propName val → do checkPropExistsAndIsEqual propName val el @@ -42,10 +52,10 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect -- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propName, val, el, extraAttributeNames } if propName == "className" - then EFn.runEffectFn2 Set.delete "class" extraAttributeNames + then EFn.runEffectFn2 deleteRequiredElement "class" extraAttributeNames else do let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` - EFn.runEffectFn2 Set.delete fullAttributeName' extraAttributeNames + EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames -- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual after" { propName, val, el, extraAttributeNames } diff --git a/src/Halogen/VDom/Set.js b/src/Halogen/VDom/Set.js index 0238524..3765301 100644 --- a/src/Halogen/VDom/Set.js +++ b/src/Halogen/VDom/Set.js @@ -14,6 +14,10 @@ exports.size = function(set) { return set.size } +exports.has = function(value) { + return set.has(value) +} + exports.toArray = function(set) { return Array.from(set) } diff --git a/src/Halogen/VDom/Set.purs b/src/Halogen/VDom/Set.purs index aded579..41b5dff 100644 --- a/src/Halogen/VDom/Set.purs +++ b/src/Halogen/VDom/Set.purs @@ -4,6 +4,7 @@ import Prelude (Unit) import Effect (Effect) import Effect.Uncurried (EffectFn2) as EFn +import Data.Function.Uncurried as Fn data Set proxy @@ -15,4 +16,6 @@ foreign import add ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit foreign import size ∷ ∀ a . Set a → Int +foreign import has ∷ ∀ a . Fn.Fn2 a (Set a) Boolean + foreign import toArray ∷ ∀ a . Set a → Array a diff --git a/test/Hydration.purs b/test/Hydration.purs index 68734e4..c66a857 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -132,47 +132,3 @@ tests = """ } ] - --- | Having `` --- | If do `$0.required = true` in chrome. --- | THE html `` --- | THE $0.attributes = { required: "true" } --- | --- | Having `` --- | If do `$0.required = false` in chrome. --- | THE html `` --- | THE $0.attributes = {} --- | --- | thus, we should check prop is set and remove it from extraAttributeNames - --- | Having `
` --- | The `$0.attributes` --- | `NamedNodeMap { id: "1", data-foo: "foo", data-bar: "bar", data-baz-bak: "baz-bak", someint: "1" }` --- | The `$0.dataset` --- | `DOMStringMap { foo: "foo", bar: "bar", bazBak: "baz-bak", someint: "1" }` --- | --- | Also, react doesnt support dataset property. Proof: --- | server = `
` --- | client = `
` --- | errorMessage = `Prop `dataset` did not match. Server: "null" Client: "[object Object]"` --- | --- | thus, react does support `data-***` attributes, but doesn't support `dataset` property, so why bother with supporting `dataset` property? --- | --- | If we wont ignore dataset, then we sould implment something like --- | --- | data PropValue = PropValue_String String | PropValue_Int Int | ... | PropValue_Dataset (Object String) --- | removePropFromExtraAttributeNames ∷ PropName → PropValue → Set → Set --- | removePropFromExtraAttributeNames propName propValue set = --- | if propName == "dataset" --- | then forEach propValue --- | (\key _val → do --- | remove ("data-" <> camelCaseToKebabCase key) set --- | ) --- | else do --- | remove (camelCaseToKebabCase key) set - --- `$0.attributes` are: --- is { required: "" } --- is { required: "false" } --- is { colspan: "1" }, but prop is colSpan = 1 ---
is { "foo:data-foo": "1" } From 42cb7258a7ee1849c1a46a9a22e0d8533380f9c1 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 13:32:11 +0300 Subject: [PATCH 24/48] fix: href property on element --- src/Halogen/VDom/DOM/Prop/Implementation.purs | 23 ++++++++++++++----- src/Halogen/VDom/Set.js | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 55ce674..602ff25 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -18,7 +18,7 @@ import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListe 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, quote) +import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM @@ -47,13 +47,24 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames pure v Property propName val → do - checkPropExistsAndIsEqual propName val el - -- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propName, val, el, extraAttributeNames } - if propName == "className" - then EFn.runEffectFn2 deleteRequiredElement "class" extraAttributeNames - else do + case propName of + "className" -> do + checkPropExistsAndIsEqual propName val el + EFn.runEffectFn2 deleteRequiredElement "class" extraAttributeNames + "href" -> do + -- | becuase on + -- | $0.href is eq to "http://localhost:3000/foo" + -- | but + -- | $0.attributes.href.value is eq to "/foo" + -- | $0.getAttribute("href") is eq to "/foo" + + -- | TODO: check it's on the element + checkAttributeExistsAndIsEqual Nothing "href" (anyToString val) el + EFn.runEffectFn2 deleteRequiredElement "href" extraAttributeNames + _ -> do + checkPropExistsAndIsEqual propName val el let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames diff --git a/src/Halogen/VDom/Set.js b/src/Halogen/VDom/Set.js index 3765301..61fec63 100644 --- a/src/Halogen/VDom/Set.js +++ b/src/Halogen/VDom/Set.js @@ -14,7 +14,7 @@ exports.size = function(set) { return set.size } -exports.has = function(value) { +exports.has = function(value, set) { return set.has(value) } From cfd471e8c1fd3b0e18c7e98642fde920ea1febcc Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 14:37:46 +0300 Subject: [PATCH 25/48] fix: error `expected false, but got true` on `` --- src/Halogen/VDom/DOM/Prop/Implementation.purs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 602ff25..92a8f04 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -14,7 +14,7 @@ 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.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropValue) import Halogen.VDom.DOM.Prop.Utils (removeProperty, setProperty, unsafeGetProperty) import Halogen.VDom.Set as Set import Halogen.VDom.Types (ElemName(..)) @@ -23,6 +23,8 @@ import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM import Web.Event.Event (EventType(..), Event) as DOM import Web.Event.EventTarget (eventListener, EventListener) as DOM +import Foreign (unsafeToForeign, typeOf) +import Unsafe.Coerce (unsafeCoerce) deleteRequiredElement :: EFn.EffectFn2 String (Set.Set String) Unit deleteRequiredElement = EFn.mkEffectFn2 \element set -> do @@ -66,7 +68,21 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect _ -> do checkPropExistsAndIsEqual propName val el let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` - EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames + case typeOf (unsafeToForeign val), (unsafeCoerce :: PropValue -> Boolean) val of + -- | if this is a boolean and is false - then it should not have been prerendered + -- | + -- | For example: + -- | `HH.button [HP.disabled false] []` should be rendered as `` + -- | `HH.button [HP.disabled true] []` should be rendered as `` + -- | + -- | Why it should NOT be rendered at all? Because + -- | `` the `$0.disabled === true` + -- | `` the `$0.disabled === true` + -- | `` the `$0.disabled === true` + -- | `` the `$0.disabled === true` + -- | `` the `$0.disabled === false` + "boolean", false -> pure unit + _, _ -> EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames -- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual after" { propName, val, el, extraAttributeNames } From 1e8a3427169b8e987e464c6cb9786b5c863c76e9 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 15:24:30 +0300 Subject: [PATCH 26/48] refactor: comment --- src/Halogen/VDom/DOM/Elem.purs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index b3684b6..31e9d1f 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -48,12 +48,10 @@ hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray let - (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- TODO + (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- HACK: not all DOM.Node's are DOM.Element's onChild :: EFn.EffectFn2 Int (DOM.Element /\ (VDom a w)) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn2 \ix (element /\ child) → do - (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child - pure res + onChild = EFn.mkEffectFn2 \ix (element /\ child) -> EFn.runEffectFn1 (hydrate element) child (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren' ch1) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let From a6f26a1db55fd94e40a9c14f4ab7736b60c70c43 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 17:32:12 +0300 Subject: [PATCH 27/48] refactor: Prop.Utils to Prop.Util --- src/Halogen/VDom/DOM/Prop.purs | 7 ++++--- src/Halogen/VDom/DOM/Prop/Checkers.purs | 2 +- src/Halogen/VDom/DOM/Prop/Implementation.purs | 2 +- src/Halogen/VDom/DOM/Prop/{Utils.purs => Util.purs} | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) rename src/Halogen/VDom/DOM/Prop/{Utils.purs => Util.purs} (97%) diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index c6485e9..1cfc5d9 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -5,20 +5,21 @@ module Halogen.VDom.DOM.Prop ) where 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.DOM.Prop.Util (propToStrKey) import Halogen.VDom.Util (STObject') - import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Effect.Uncurried as EFn import Foreign.Object as Object -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.Util as Util import Halogen.VDom.DOM.Prop.Checkers (mkExtraAttributeNames, checkExtraAttributeNamesIsEmpty) +import Halogen.VDom.DOM.Prop.Types (Prop(..), ElemRef(..), PropValue, propFromString, propFromBoolean, propFromInt, propFromNumber) as Export + hydrateProp ∷ ∀ a . BuildPropFunction a diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 52e8624..377cd90 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -12,7 +12,7 @@ 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.DOM.Prop.Util (unsafeGetProperty) import Halogen.VDom.Set as Set import Halogen.VDom.Types (ElemName(..), Namespace) import Halogen.VDom.Util (anyToString, fullAttributeName, quote, warnAny) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 92a8f04..07d2cb1 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -15,7 +15,7 @@ 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(..), PropValue) -import Halogen.VDom.DOM.Prop.Utils (removeProperty, setProperty, unsafeGetProperty) +import Halogen.VDom.DOM.Prop.Util (removeProperty, setProperty, unsafeGetProperty) import Halogen.VDom.Set as Set import Halogen.VDom.Types (ElemName(..)) import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) diff --git a/src/Halogen/VDom/DOM/Prop/Utils.purs b/src/Halogen/VDom/DOM/Prop/Util.purs similarity index 97% rename from src/Halogen/VDom/DOM/Prop/Utils.purs rename to src/Halogen/VDom/DOM/Prop/Util.purs index 9fad7dc..068395a 100644 --- a/src/Halogen/VDom/DOM/Prop/Utils.purs +++ b/src/Halogen/VDom/DOM/Prop/Util.purs @@ -1,4 +1,4 @@ -module Halogen.VDom.DOM.Prop.Utils where +module Halogen.VDom.DOM.Prop.Util where import Prelude (Unit, (<>), (>>=)) From 8c0fe08c232f92878c72b055734ff1708372e85b Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 17:37:01 +0300 Subject: [PATCH 28/48] feat: normalizeChildren --- src/Halogen/VDom/DOM/Elem.purs | 14 +++++++++----- src/Halogen/VDom/DOM/Keyed.purs | 4 ++-- src/Halogen/VDom/DOM/Util.purs | 19 +++++++++++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 src/Halogen/VDom/DOM/Util.purs diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 31e9d1f..daa964c 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -1,11 +1,11 @@ module Halogen.VDom.DOM.Elem where +import Prelude + import Data.Tuple.Nested (type (/\), (/\)) import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) -import Prelude - -import Data.Array (length, zip) as Array +import Data.Array (length, zip, fromFoldable) as Array import Data.Function.Uncurried as Fn import Data.Maybe (Maybe) import Data.Nullable (toNullable) @@ -13,6 +13,7 @@ import Effect.Uncurried as EFn import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) import Halogen.VDom.Util as Util +import Halogen.VDom.DOM.Util as DOMUtil import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element @@ -38,9 +39,12 @@ hydrateElem a w hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do + let + normalizedChildren = DOMUtil.normalizeChildren ch1 + checkIsElementNode currentElement checkTagNameIsEqualTo ns1 name1 currentElement - checkChildrenLengthIsEqualTo (Array.length ch1) currentElement + checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement let currentNode :: DOM.Node currentNode = DOM.Element.toNode currentElement @@ -52,7 +56,7 @@ hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 onChild :: EFn.EffectFn2 Int (DOM.Element /\ (VDom a w)) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn2 \ix (element /\ child) -> EFn.runEffectFn1 (hydrate element) child - (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren' ch1) onChild + (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren' normalizedChildren) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let state = diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 996c2e1..c42946b 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -40,7 +40,7 @@ hydrateKeyed (Array (Tuple String (VDom a w))) a w -hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do +hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do -- TODO: normalizeChildren checkIsElementNode currentElement checkTagNameIsEqualTo ns1 name1 currentElement checkChildrenLengthIsEqualTo (Array.length keyedChildren) currentElement @@ -51,7 +51,7 @@ hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray let - (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- TODO + (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- HACK: not all DOM.Node's are DOM.Element's onChild :: EFn.EffectFn3 String Int ({ element ∷ DOM.Element, keyedChild ∷ Tuple String (VDom a w) }) (Step (VDom a w) DOM.Node) onChild = EFn.mkEffectFn3 \k ix ({ element, keyedChild: Tuple _ child }) → do diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs new file mode 100644 index 0000000..65b13c7 --- /dev/null +++ b/src/Halogen/VDom/DOM/Util.purs @@ -0,0 +1,19 @@ +module Halogen.VDom.DOM.Util where + +import Prelude + +import Data.Array (fromFoldable) as Array +import Halogen.VDom.Types (VDom(..)) +import Data.List as List +import Data.List (List(..), (:)) + +-- | e.g. +-- | [Text ""] -> [] +-- | [Text "foo", Text "bar"] -> [Text "foobar"] +normalizeChildren :: forall a w . Array (VDom a w) -> Array (VDom a w) +normalizeChildren = Array.fromFoldable <<< List.foldr go Nil <<< List.fromFoldable + where + go :: VDom a w -> List (VDom a w) -> List (VDom a w) + go (Text "") accum = accum + go (Text text2) (Text text1 : accumt) = Text (text1 <> text2) : accumt + go vdom accum = vdom : accum From 1eba0b7fe1cd41548c31981fb16a0cb6ef08c059 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 20:17:20 +0300 Subject: [PATCH 29/48] fix: use Node instead of Element for root node, more type safety --- src/Halogen/VDom/DOM.purs | 3 +- src/Halogen/VDom/DOM/Checkers.purs | 55 +++++++++++++++--------------- src/Halogen/VDom/DOM/Elem.purs | 15 +++----- src/Halogen/VDom/DOM/Keyed.purs | 14 ++++---- src/Halogen/VDom/DOM/Text.purs | 10 ++---- src/Halogen/VDom/DOM/Types.purs | 10 +++--- src/Halogen/VDom/Thunk.purs | 3 +- test/Hydration.purs | 3 +- 8 files changed, 51 insertions(+), 62 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index f462ac9..fd7d17c 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -20,8 +20,9 @@ import Halogen.VDom.DOM.Types (VDomSpec(..)) as Export import Halogen.VDom.DOM.Widget (buildWidget) as Export import Halogen.VDom.Types (VDom(..), runGraft) import Web.DOM.Element (Element) as DOM +import Web.DOM.Node (Node) as DOM -hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Element -> VDomMachine a w +hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Node -> VDomMachine a w hydrateVDom spec rootNode = hydrate rootNode where build = buildVDom spec diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index f0c588c..1119110 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -1,8 +1,8 @@ module Halogen.VDom.DOM.Checkers where -import Prelude (Unit, bind, discard, show, when, ($), (/=), (<>)) +import Prelude -import Data.Maybe (Maybe) +import Data.Maybe (Maybe(..)) import Data.String (toUpper) import Effect (Effect) import Effect.Exception (error, throwException) @@ -12,44 +12,43 @@ import Halogen.VDom.Util (fullAttributeName, quote, warnAny) import Partial.Unsafe (unsafePartial) import Web.DOM (NodeList, NodeType) as DOM import Web.DOM.Element (Element, tagName) as DOM +import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.Node (childNodes, nodeType, textContent) as DOM +import Web.DOM.Node (childNodes, nodeType) as DOM.Node +import Web.DOM.Node (Node) as DOM import Web.DOM.NodeList (length) as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType +import Web.DOM.Text as DOM.Text +import Web.DOM.Text (Text) as DOM -------------------------------------- -- Text +checkIsTextNode :: DOM.Node -> Effect DOM.Text +checkIsTextNode node = + case DOM.Text.fromNode node of + Just text -> pure text + Nothing -> do + EFn.runEffectFn2 warnAny "Error at " { node } + throwException $ error $ "Expected node to be a " <> show DOM.NodeType.TextNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) -checkElementIsNodeType :: DOM.NodeType -> DOM.Element -> Effect Unit -checkElementIsNodeType = checkElementIsNodeType' - where - getElementNodeType :: DOM.Element -> DOM.NodeType - getElementNodeType element = unsafePartial $ DOM.nodeType (DOM.Element.toNode element) - - checkElementIsNodeType' :: DOM.NodeType -> DOM.Element -> Effect Unit - checkElementIsNodeType' expectedNodeType element = do - let nodeType = getElementNodeType element - EFn.runEffectFn2 warnAny "checkElementIsNodeType" { nodeType, expectedNodeType, meta: { element } } - when (nodeType /= expectedNodeType) (do - EFn.runEffectFn2 warnAny "Error at " { element } - throwException $ error $ "Expected element to be a " <> show expectedNodeType <> ", but got " <> show nodeType) - -checkIsTextNode :: DOM.Element -> Effect Unit -checkIsTextNode = checkElementIsNodeType DOM.NodeType.TextNode - -checkTextContentIsEqTo :: String -> DOM.Element -> Effect Unit -checkTextContentIsEqTo expectedText element = do - textContent <- DOM.textContent (DOM.Element.toNode element) - EFn.runEffectFn2 warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { element } } +checkTextContentIsEqTo :: String -> DOM.Text -> Effect Unit +checkTextContentIsEqTo expectedText text = do + textContent <- DOM.Text.wholeText text + EFn.runEffectFn2 warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { text } } when (textContent /= expectedText) (do throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) -------------------------------------- -- Elem -checkIsElementNode :: DOM.Element -> Effect Unit -checkIsElementNode = checkElementIsNodeType DOM.NodeType.ElementNode +checkIsElementNode :: DOM.Node -> Effect DOM.Element +checkIsElementNode node = + case DOM.Element.fromNode node of + Just text -> pure text + Nothing -> do + EFn.runEffectFn2 warnAny "Error at " { node } + throwException $ error $ "Expected node to be a " <> show DOM.NodeType.ElementNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit checkTagNameIsEqualTo maybeNamespace elemName element = do @@ -57,14 +56,14 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do -- e.g. `DIV` or `FOO:SVG` expectedTagName :: String expectedTagName = toUpper $ fullAttributeName maybeNamespace elemName - let tagName = DOM.tagName element + let tagName = DOM.Element.tagName element EFn.runEffectFn2 warnAny "checkTagNameIsEqualTo" { expectedTagName, tagName, meta: { maybeNamespace, elemName, element } } when (tagName /= expectedTagName) (do throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do - (elementChildren :: DOM.NodeList) <- DOM.childNodes (DOM.Element.toNode element) + (elementChildren :: DOM.NodeList) <- DOM.Node.childNodes (DOM.Element.toNode element) elementChildrenLength <- DOM.NodeList.length elementChildren EFn.runEffectFn2 warnAny "checkChildrenLengthIsEqualTo" { elementChildrenLength, expectedLength, meta: { element, elementChildren } } when (elementChildrenLength /= expectedLength) do diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index daa964c..ba710c9 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -38,25 +38,20 @@ hydrateElem (Array (VDom a w)) a w -hydrateElem = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do +hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do let normalizedChildren = DOMUtil.normalizeChildren ch1 - checkIsElementNode currentElement + currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement - let - currentNode :: DOM.Node - currentNode = DOM.Element.toNode currentElement (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray let - (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- HACK: not all DOM.Node's are DOM.Element's - - onChild :: EFn.EffectFn2 Int (DOM.Element /\ (VDom a w)) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn2 \ix (element /\ child) -> EFn.runEffectFn1 (hydrate element) child - (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren' normalizedChildren) onChild + onChild :: EFn.EffectFn2 Int (DOM.Node /\ (VDom a w)) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn2 \ix (node /\ child) -> EFn.runEffectFn1 (hydrate node) child + (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren normalizedChildren) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let state = diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index c42946b..1969627 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -40,8 +40,8 @@ hydrateKeyed (Array (Tuple String (VDom a w))) a w -hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do -- TODO: normalizeChildren - checkIsElementNode currentElement +hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do -- TODO: normalizeChildren + currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement checkChildrenLengthIsEqualTo (Array.length keyedChildren) currentElement let @@ -51,16 +51,14 @@ hydrateKeyed = EFn.mkEffectFn8 \currentElement (VDomSpec spec) hydrate build ns1 (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray let - (currentElementChildren' :: Array DOM.Element) = unsafeCoerce currentElementChildren -- HACK: not all DOM.Node's are DOM.Element's - - onChild :: EFn.EffectFn3 String Int ({ element ∷ DOM.Element, keyedChild ∷ Tuple String (VDom a w) }) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn3 \k ix ({ element, keyedChild: Tuple _ child }) → do - (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate element) child + onChild :: EFn.EffectFn3 String Int ({ node ∷ DOM.Node, keyedChild ∷ Tuple String (VDom a w) }) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn3 \k ix ({ node, keyedChild: Tuple _ child }) → do + (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate node) child pure res (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE - (Array.zipWith (\element keyedChild → { element, keyedChild }) currentElementChildren' keyedChildren) + (Array.zipWith (\node keyedChild → { node, keyedChild }) currentElementChildren keyedChildren) (\{ keyedChild } → fst keyedChild) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index dee0e1f..15078db 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -19,13 +19,9 @@ type TextState a w = -- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w -hydrateText = EFn.mkEffectFn5 \currentElement (VDomSpec spec) _hydrate build s → do - let - currentNode :: DOM.Node - currentNode = DOM.Element.toNode currentElement - - checkIsTextNode currentElement - checkTextContentIsEqTo s currentElement +hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → do + currentText <- checkIsTextNode currentNode + checkTextContentIsEqTo s currentText let (state :: TextState a w) = { build, node: currentNode, value: s } pure $ mkStep $ Step currentNode state patchText haltText diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index 0367976..c92b13c 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -19,9 +19,9 @@ type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomS type VDomHydrator i a w = EFn.EffectFn5 - DOM.Element -- current element + DOM.Node -- current element (VDomSpec a w) - (DOM.Element -> VDomMachine a w) -- top hydrate function + (DOM.Node -> VDomMachine a w) -- top hydrate function (VDomMachine a w) -- top build function i (VDomStep a w) @@ -30,9 +30,9 @@ type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i type VDomHydrator4 i j k l a w = EFn.EffectFn8 - DOM.Element + DOM.Node (VDomSpec a w) - (DOM.Element -> VDomMachine a w) + (DOM.Node -> VDomMachine a w) (VDomMachine a w) i j @@ -44,7 +44,7 @@ type VDomHydrator4 i j k l a w -- | enable recursive trees of Widgets. newtype VDomSpec a w = VDomSpec { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` - , hydrateWidget ∷ VDomSpec a w → DOM.Element → Machine w DOM.Node + , hydrateWidget ∷ VDomSpec a w → DOM.Node → Machine w DOM.Node , buildAttributes ∷ DOM.Element → Machine a Unit , hydrateAttributes ∷ DOM.Element → Machine a Unit diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index abc149d..3825291 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -21,7 +21,6 @@ import Halogen.VDom.Util as Util import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Node (Node) import Halogen.VDom.DOM.Types (VDomMachine) -import Web.DOM.Element (Element) as DOM foreign import data ThunkArg ∷ Type @@ -103,7 +102,7 @@ hydrateThunk ∷ ∀ f i a w . (f i → V.VDom a w) → V.VDomSpec a w - → DOM.Element + → Node → V.Machine (Thunk f i) Node hydrateThunk toVDom spec element = mkThunkBuilder (V.hydrateVDom spec element) toVDom diff --git a/test/Hydration.purs b/test/Hydration.purs index c66a857..1776a75 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -11,6 +11,7 @@ import Effect.Uncurried as EFn import Halogen.VDom as V import Halogen.VDom.Finders (findElementFirstChild) import Web.DOM.ParentNode (ParentNode) +import Web.DOM.Element as DOM.Element import Web.DOM (Element) import Web.DOM.ParentNode (querySelector, QuerySelector(..)) as DOM import Data.Maybe (maybe) @@ -67,7 +68,7 @@ main = do initialValue = initialState render = renderData initialVdom = un VDom (render initialValue) - machine ← EFn.runEffectFn1 (V.hydrateVDom spec rootElement) initialVdom + machine ← EFn.runEffectFn1 (V.hydrateVDom spec (DOM.Element.toNode rootElement)) initialVdom listener ← DOM.eventListener \_ev → void $ EFn.runEffectFn2 V.step machine (un VDom (render state2)) From baab1d75d7379c7a5ac60806ac22dde2ad17e0bc Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 16 Jun 2020 20:35:55 +0300 Subject: [PATCH 30/48] fix: normalizeChildren --- src/Halogen/VDom/DOM/Util.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index 65b13c7..e02501d 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -15,5 +15,5 @@ normalizeChildren = Array.fromFoldable <<< List.foldr go Nil <<< List.fromFoldab where go :: VDom a w -> List (VDom a w) -> List (VDom a w) go (Text "") accum = accum - go (Text text2) (Text text1 : accumt) = Text (text1 <> text2) : accumt + go (Text text1) (Text text2 : accumt) = Text (text1 <> text2) : accumt go vdom accum = vdom : accum From d074b4101cd079bd918c39cdd2764e65376b4899 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 17 Jun 2020 16:14:24 +0300 Subject: [PATCH 31/48] fix: hydration -> hydrateElement -> split text on dom according to vdom during hydration (this pr matches test in https://github.com/srghma/purescript-halogen-nextjs/commit/63a9401d493c2e12d5a11c4d6c08fda4fe6f0d0c) --- src/Halogen/VDom/DOM/Checkers.purs | 29 +++---- src/Halogen/VDom/DOM/Elem.purs | 107 +++++++++++++++++++++--- src/Halogen/VDom/DOM/Prop/Checkers.purs | 15 ++-- 3 files changed, 117 insertions(+), 34 deletions(-) diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 1119110..193512b 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -8,18 +8,15 @@ import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Halogen.VDom.Types (ElemName, Namespace) -import Halogen.VDom.Util (fullAttributeName, quote, warnAny) +import Halogen.VDom.Util as Util import Partial.Unsafe (unsafePartial) -import Web.DOM (NodeList, NodeType) as DOM -import Web.DOM.Element (Element, tagName) as DOM -import Web.DOM.Element (Element) as DOM +import Web.DOM as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.Node (childNodes, nodeType) as DOM.Node -import Web.DOM.Node (Node) as DOM -import Web.DOM.NodeList (length) as DOM.NodeList +import Web.DOM.Node as DOM.Node +import Web.DOM.NodeList as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType import Web.DOM.Text as DOM.Text -import Web.DOM.Text (Text) as DOM +import Web.DOM.CharacterData as DOM.CharacterData -------------------------------------- -- Text @@ -29,15 +26,15 @@ checkIsTextNode node = case DOM.Text.fromNode node of Just text -> pure text Nothing -> do - EFn.runEffectFn2 warnAny "Error at " { node } + EFn.runEffectFn2 Util.warnAny "Error at " { node } throwException $ error $ "Expected node to be a " <> show DOM.NodeType.TextNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) checkTextContentIsEqTo :: String -> DOM.Text -> Effect Unit checkTextContentIsEqTo expectedText text = do - textContent <- DOM.Text.wholeText text - EFn.runEffectFn2 warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { text } } + textContent <- DOM.CharacterData.data_ (DOM.Text.toCharacterData text) + EFn.runEffectFn2 Util.warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { text } } when (textContent /= expectedText) (do - throwException $ error $ "Expected element text content to equal to " <> quote expectedText <> ", but got " <> quote textContent) + throwException $ error $ "Expected element text content to equal to " <> Util.quote expectedText <> ", but got " <> Util.quote textContent) -------------------------------------- -- Elem @@ -47,7 +44,7 @@ checkIsElementNode node = case DOM.Element.fromNode node of Just text -> pure text Nothing -> do - EFn.runEffectFn2 warnAny "Error at " { node } + EFn.runEffectFn2 Util.warnAny "Error at " { node } throwException $ error $ "Expected node to be a " <> show DOM.NodeType.ElementNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit @@ -55,9 +52,9 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do let -- e.g. `DIV` or `FOO:SVG` expectedTagName :: String - expectedTagName = toUpper $ fullAttributeName maybeNamespace elemName + expectedTagName = toUpper $ Util.fullAttributeName maybeNamespace elemName let tagName = DOM.Element.tagName element - EFn.runEffectFn2 warnAny "checkTagNameIsEqualTo" { expectedTagName, tagName, meta: { maybeNamespace, elemName, element } } + EFn.runEffectFn2 Util.warnAny "checkTagNameIsEqualTo" { expectedTagName, tagName, meta: { maybeNamespace, elemName, element } } when (tagName /= expectedTagName) (do throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) @@ -65,6 +62,6 @@ checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit checkChildrenLengthIsEqualTo expectedLength element = do (elementChildren :: DOM.NodeList) <- DOM.Node.childNodes (DOM.Element.toNode element) elementChildrenLength <- DOM.NodeList.length elementChildren - EFn.runEffectFn2 warnAny "checkChildrenLengthIsEqualTo" { elementChildrenLength, expectedLength, meta: { element, elementChildren } } + EFn.runEffectFn2 Util.warnAny "checkChildrenLengthIsEqualTo" { elementChildrenLength, expectedLength, meta: { element, elementChildren } } when (elementChildrenLength /= expectedLength) do (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index ba710c9..bef418e 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -7,18 +7,28 @@ import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpe import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) import Data.Array (length, zip, fromFoldable) as Array import Data.Function.Uncurried as Fn -import Data.Maybe (Maybe) +import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) import Effect.Uncurried as EFn +import Effect (Effect) import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) import Halogen.VDom.Util as Util import Halogen.VDom.DOM.Util as DOMUtil import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Element (Element) as DOM +import Web.DOM as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.Node (Node, childNodes) as DOM +import Web.DOM.Node as DOM.Node +import Web.DOM.Text as DOM.Text import Web.DOM.NodeList as DOM.NodeList +import Web.DOM.Document as DOM.Document +import Web.DOM.CharacterData as DOM.CharacterData +import Data.List (List(..), (:)) +import Data.List as List +import Data.String as String +import Effect.Exception (error, throwException) +import Data.Traversable (for) +import Control.Alt ((<|>)) type ElemState a w = { build ∷ VDomMachine a w @@ -29,6 +39,79 @@ type ElemState a w = , children ∷ Array (VDomStep a w) } +data ElementOrTextNode = ElementNode DOM.Element | TextNode DOM.Text + +elementOrTextNodeToNode :: ElementOrTextNode -> DOM.Node +elementOrTextNodeToNode referenceNode = + case referenceNode of + ElementNode element -> DOM.Element.toNode element + TextNode text -> DOM.Text.toNode text + +-- | The idea is to prevent rerendering on the next render +-- | but because in the prerendered html all text nodes are merged (`HH.div_ [ HH.text "foo", HH.text "bar" ]` rendered as `
foobar
`, not `
"foo""bar"
`) +-- | and empty text nodes "" are hidden (i.e. they exist in $0.childNodes, but not rendered), +-- | we need to split text nodes using .splitText() and insert "" nodes where it is needed +-- | +-- | check https://jsbin.com/bukulicito/edit?html,output to see how text nodes are added to the parent +-- | +-- | How textNode.splitText() works: +-- | 1. when $0 is '
foobar
' -> $0.childNodes[0].splitText(0) -> returns "foobar", does nothing +-- | 2. when $0 is '
foobar
' -> $0.childNodes[0].splitText(3) -> splits on "foo" and "bar", returns "bar" +-- | 3. when $0 is '
foobar
' -> $0.childNodes[0].splitText(6) -> adds new node "" after "foobar", returns "" +-- | 4. when $0 is '
foobar
' -> $0.childNodes[0].splitText(100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 100 is larger than the Text node's length." +-- | 5. when $0 is '
foobar
' -> $0.childNodes[0].splitText(-100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 4294966996 is larger than the Text node's length." +zipChildrenAndSplitTextNodes :: forall a w . VDomSpec a w -> DOM.Node -> List ElementOrTextNode -> List (VDom a w) -> Effect (List { node :: ElementOrTextNode, vdom :: VDom a w }) +zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildren ((Text "") : vdomChildrenTail) = do + -- | when DOM is `
foo
` and vdom is `HH.div_ [HH.text "foo", HH.text ""]` - it wont touch the "foo", but should append new text node "" after "foo" + -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } + + (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document + + case domChildren of + Nil -> void $ DOM.Node.appendChild (DOM.Text.toNode newChildWithEmptyText) parent + (referenceNode : _) -> do + let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode + void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent + + tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildren vdomChildrenTail + pure ({ node: TextNode newChildWithEmptyText, vdom: Text "" } : tailResult) +zipChildrenAndSplitTextNodes (VDomSpec spec) parent (TextNode textNode : domChildrenTail) ((Text expectedText) : vdomChildrenTail) = do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } + textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) + let expectedTextLength = String.length expectedText + + case compare textNodeLength expectedTextLength of + LT -> do + textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } + throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors + + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate + EQ -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } + tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildrenTail vdomChildrenTail + pure ({ node: TextNode textNode, vdom: Text expectedText } : tailResult) + + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" + GT -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } + nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") + tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail + pure ({ node: TextNode textNode, vdom: Text expectedText } : tailResult) +zipChildrenAndSplitTextNodes spec parent (domChild : domChildrenTail) (vdomChild : vdomChildrenTail) = do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} + tailResult <- zipChildrenAndSplitTextNodes spec parent domChildrenTail vdomChildrenTail + pure ({ node: domChild, vdom: vdomChild } : tailResult) +zipChildrenAndSplitTextNodes spec parent Nil Nil = pure Nil +zipChildrenAndSplitTextNodes spec parent otherDomChildren otherVdomChildren = throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" + +toElementOrTextNode :: DOM.Node -> Maybe ElementOrTextNode +toElementOrTextNode node = (DOM.Text.fromNode node <#> TextNode) <|> (DOM.Element.fromNode node <#> ElementNode) + +listToElementOrTextNode :: List DOM.Node -> List ElementOrTextNode +listToElementOrTextNode = map toElementOrTextNode >>> List.catMaybes + hydrateElem ∷ ∀ a w . VDomHydrator4 @@ -39,19 +122,23 @@ hydrateElem a w hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do - let - normalizedChildren = DOMUtil.normalizeChildren ch1 + -- | let + -- | normalizedChildren = DOMUtil.normalizeChildren ch1 currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement - checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement + -- | checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement + + (currentElementChildren :: List DOM.Node) <- DOM.Node.childNodes currentNode >>= DOM.NodeList.toArray <#> List.fromFoldable + + let (currentElementChildren' :: List ElementOrTextNode) = listToElementOrTextNode currentElementChildren - (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray + (zippedChildren :: List { node :: ElementOrTextNode, vdom :: VDom a w }) <- zipChildrenAndSplitTextNodes (VDomSpec spec) currentNode currentElementChildren' (List.fromFoldable ch1) let - onChild :: EFn.EffectFn2 Int (DOM.Node /\ (VDom a w)) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn2 \ix (node /\ child) -> EFn.runEffectFn1 (hydrate node) child - (children :: Array (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn2 Util.forE (Array.zip currentElementChildren normalizedChildren) onChild + onChild :: { node :: ElementOrTextNode, vdom :: VDom a w } -> Effect (Step (VDom a w) DOM.Node) + onChild { node, vdom } = EFn.runEffectFn1 (hydrate (elementOrTextNodeToNode node)) vdom + (children :: Array (Step (VDom a w) DOM.Node)) <- for zippedChildren onChild <#> Array.fromFoldable (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let state = diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 377cd90..eed5f35 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -15,7 +15,6 @@ import Halogen.VDom.DOM.Prop.Types (PropValue) import Halogen.VDom.DOM.Prop.Util (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 @@ -24,20 +23,20 @@ checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe case elementValue of Nothing → do - EFn.runEffectFn2 warnAny "checkAttributeExistsAndIsEqual -> missing" { element } - throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it is missing" + EFn.runEffectFn2 Util.warnAny "checkAttributeExistsAndIsEqual -> missing" { element } + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it is missing" Just elementValue' → do - EFn.runEffectFn2 warnAny "checkAttributeExistsAndIsEqual -> not missing" { elementValue', expectedElementValue, meta: { maybeNamespace, attributeName, expectedElementValue, element } } + EFn.runEffectFn2 Util.warnAny "checkAttributeExistsAndIsEqual -> not missing" { elementValue', expectedElementValue, meta: { maybeNamespace, attributeName, expectedElementValue, element } } unless (elementValue' == expectedElementValue) (do - throwException $ error $ "Expected element to have an attribute " <> quote (fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> quote expectedElementValue <> ", but it was equal to " <> quote elementValue' + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it was equal to " <> Util.quote elementValue' ) checkPropExistsAndIsEqual ∷ String → PropValue → DOM.Element → Effect Unit checkPropExistsAndIsEqual propName expectedPropValue element = do let propValue = Fn.runFn2 unsafeGetProperty propName element - EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propValue, expectedPropValue, meta: { element, propName } } + EFn.runEffectFn2 Util.warnAny "checkPropExistsAndIsEqual" { propValue, expectedPropValue, meta: { element, propName } } unless (Fn.runFn2 Util.refEq propValue expectedPropValue) (do - throwException $ error $ "Expected element to have a prop " <> quote propName <> " eq to " <> quote (anyToString expectedPropValue) <> ", but it was equal to " <> quote (anyToString propValue) + throwException $ error $ "Expected element to have a prop " <> Util.quote propName <> " eq to " <> Util.quote (Util.anyToString expectedPropValue) <> ", but it was equal to " <> Util.quote (Util.anyToString propValue) ) -- | Inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030 @@ -51,7 +50,7 @@ mkExtraAttributeNames el = do checkExtraAttributeNamesIsEmpty ∷ Set.Set String -> DOM.Element -> Effect Unit checkExtraAttributeNamesIsEmpty extraAttributeNames element = do - EFn.runEffectFn2 warnAny "checkExtraAttributeNamesIsEmpty" { extraAttributeNames, meta: { element } } + EFn.runEffectFn2 Util.warnAny "checkExtraAttributeNamesIsEmpty" { extraAttributeNames, meta: { element } } when (Set.size extraAttributeNames > 0) (do throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") ) From 30966ef981363eedc336e09fdc318eb680303863 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 17 Jun 2020 18:10:38 +0300 Subject: [PATCH 32/48] fix: hydration -> hydrateKeyed -> split text on dom according to vdom during hydration --- src/Halogen/VDom/DOM/Elem.purs | 91 +++-------------------- src/Halogen/VDom/DOM/Keyed.purs | 38 ++++++---- src/Halogen/VDom/DOM/Util.purs | 127 +++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 105 deletions(-) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index bef418e..bccc69e 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -39,79 +39,6 @@ type ElemState a w = , children ∷ Array (VDomStep a w) } -data ElementOrTextNode = ElementNode DOM.Element | TextNode DOM.Text - -elementOrTextNodeToNode :: ElementOrTextNode -> DOM.Node -elementOrTextNodeToNode referenceNode = - case referenceNode of - ElementNode element -> DOM.Element.toNode element - TextNode text -> DOM.Text.toNode text - --- | The idea is to prevent rerendering on the next render --- | but because in the prerendered html all text nodes are merged (`HH.div_ [ HH.text "foo", HH.text "bar" ]` rendered as `
foobar
`, not `
"foo""bar"
`) --- | and empty text nodes "" are hidden (i.e. they exist in $0.childNodes, but not rendered), --- | we need to split text nodes using .splitText() and insert "" nodes where it is needed --- | --- | check https://jsbin.com/bukulicito/edit?html,output to see how text nodes are added to the parent --- | --- | How textNode.splitText() works: --- | 1. when $0 is '
foobar
' -> $0.childNodes[0].splitText(0) -> returns "foobar", does nothing --- | 2. when $0 is '
foobar
' -> $0.childNodes[0].splitText(3) -> splits on "foo" and "bar", returns "bar" --- | 3. when $0 is '
foobar
' -> $0.childNodes[0].splitText(6) -> adds new node "" after "foobar", returns "" --- | 4. when $0 is '
foobar
' -> $0.childNodes[0].splitText(100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 100 is larger than the Text node's length." --- | 5. when $0 is '
foobar
' -> $0.childNodes[0].splitText(-100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 4294966996 is larger than the Text node's length." -zipChildrenAndSplitTextNodes :: forall a w . VDomSpec a w -> DOM.Node -> List ElementOrTextNode -> List (VDom a w) -> Effect (List { node :: ElementOrTextNode, vdom :: VDom a w }) -zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildren ((Text "") : vdomChildrenTail) = do - -- | when DOM is `
foo
` and vdom is `HH.div_ [HH.text "foo", HH.text ""]` - it wont touch the "foo", but should append new text node "" after "foo" - -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } - - (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document - - case domChildren of - Nil -> void $ DOM.Node.appendChild (DOM.Text.toNode newChildWithEmptyText) parent - (referenceNode : _) -> do - let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode - void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent - - tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildren vdomChildrenTail - pure ({ node: TextNode newChildWithEmptyText, vdom: Text "" } : tailResult) -zipChildrenAndSplitTextNodes (VDomSpec spec) parent (TextNode textNode : domChildrenTail) ((Text expectedText) : vdomChildrenTail) = do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } - textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) - let expectedTextLength = String.length expectedText - - case compare textNodeLength expectedTextLength of - LT -> do - textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } - throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors - - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate - EQ -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } - tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent domChildrenTail vdomChildrenTail - pure ({ node: TextNode textNode, vdom: Text expectedText } : tailResult) - - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" - GT -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } - nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") - tailResult <- zipChildrenAndSplitTextNodes (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail - pure ({ node: TextNode textNode, vdom: Text expectedText } : tailResult) -zipChildrenAndSplitTextNodes spec parent (domChild : domChildrenTail) (vdomChild : vdomChildrenTail) = do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} - tailResult <- zipChildrenAndSplitTextNodes spec parent domChildrenTail vdomChildrenTail - pure ({ node: domChild, vdom: vdomChild } : tailResult) -zipChildrenAndSplitTextNodes spec parent Nil Nil = pure Nil -zipChildrenAndSplitTextNodes spec parent otherDomChildren otherVdomChildren = throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" - -toElementOrTextNode :: DOM.Node -> Maybe ElementOrTextNode -toElementOrTextNode node = (DOM.Text.fromNode node <#> TextNode) <|> (DOM.Element.fromNode node <#> ElementNode) - -listToElementOrTextNode :: List DOM.Node -> List ElementOrTextNode -listToElementOrTextNode = map toElementOrTextNode >>> List.catMaybes - hydrateElem ∷ ∀ a w . VDomHydrator4 @@ -122,22 +49,26 @@ hydrateElem a w hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do - -- | let - -- | normalizedChildren = DOMUtil.normalizeChildren ch1 - currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement -- | checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement (currentElementChildren :: List DOM.Node) <- DOM.Node.childNodes currentNode >>= DOM.NodeList.toArray <#> List.fromFoldable - let (currentElementChildren' :: List ElementOrTextNode) = listToElementOrTextNode currentElementChildren + let (currentElementChildren' :: List DOMUtil.ElementOrTextNode) = DOMUtil.listToElementOrTextNode currentElementChildren - (zippedChildren :: List { node :: ElementOrTextNode, vdom :: VDom a w }) <- zipChildrenAndSplitTextNodes (VDomSpec spec) currentNode currentElementChildren' (List.fromFoldable ch1) + (zippedChildren :: List { node :: DOM.Node, vdom :: VDom a w }) <- + DOMUtil.zipChildrenAndSplitTextNodes + (\(node :: DOMUtil.ElementOrTextNode) (vdom :: VDom a w) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom }) + identity + (VDomSpec spec) + currentNode + currentElementChildren' + (List.fromFoldable ch1) let - onChild :: { node :: ElementOrTextNode, vdom :: VDom a w } -> Effect (Step (VDom a w) DOM.Node) - onChild { node, vdom } = EFn.runEffectFn1 (hydrate (elementOrTextNodeToNode node)) vdom + onChild :: { node :: DOM.Node, vdom :: VDom a w } -> Effect (Step (VDom a w) DOM.Node) + onChild { node, vdom } = EFn.runEffectFn1 (hydrate node) vdom (children :: Array (Step (VDom a w) DOM.Node)) <- for zippedChildren onChild <#> Array.fromFoldable (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 1969627..6dc0dda 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -1,12 +1,12 @@ module Halogen.VDom.DOM.Keyed where -import Prelude (Unit, bind, discard, pure, ($), (>>=)) +import Prelude import Data.Array as Array import Data.Function.Uncurried as Fn import Data.Maybe (Maybe) import Data.Nullable (toNullable) -import Data.Tuple (Tuple(..), fst) +import Data.Tuple (Tuple(..), fst, snd) import Effect.Uncurried as EFn import Foreign.Object as Object import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) @@ -19,7 +19,10 @@ import Web.DOM.NodeList as DOM.NodeList import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Node (Node, childNodes) as DOM import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) -import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) +import Halogen.VDom.DOM.Checkers (checkIsElementNode, checkTagNameIsEqualTo) +import Data.List (List(..), (:)) +import Data.List as List +import Halogen.VDom.DOM.Util as DOMUtil type KeyedState a w = { build ∷ VDomMachine a w @@ -40,26 +43,31 @@ hydrateKeyed (Array (Tuple String (VDom a w))) a w -hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do -- TODO: normalizeChildren +hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement - checkChildrenLengthIsEqualTo (Array.length keyedChildren) currentElement - let - currentNode :: DOM.Node - currentNode = DOM.Element.toNode currentElement - (currentElementChildren :: Array DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray + (currentElementChildren :: List DOM.Node) <- DOM.childNodes currentNode >>= DOM.NodeList.toArray <#> List.fromFoldable + + let (currentElementChildren' :: List DOMUtil.ElementOrTextNode) = DOMUtil.listToElementOrTextNode currentElementChildren + + (zippedChildren :: List { node :: DOM.Node, vdom :: VDom a w, key :: String }) <- + DOMUtil.zipChildrenAndSplitTextNodes + (\(node :: DOMUtil.ElementOrTextNode) (Tuple key vdom) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom, key }) + snd + (VDomSpec spec) + currentNode + currentElementChildren' + (List.fromFoldable keyedChildren) let - onChild :: EFn.EffectFn3 String Int ({ node ∷ DOM.Node, keyedChild ∷ Tuple String (VDom a w) }) (Step (VDom a w) DOM.Node) - onChild = EFn.mkEffectFn3 \k ix ({ node, keyedChild: Tuple _ child }) → do - (res :: Step (VDom a w) DOM.Node) ← EFn.runEffectFn1 (hydrate node) child - pure res + onChild :: EFn.EffectFn3 String Int ({ node :: DOM.Node, vdom :: VDom a w, key :: String }) (Step (VDom a w) DOM.Node) + onChild = EFn.mkEffectFn3 \k ix ({ node, vdom }) → EFn.runEffectFn1 (hydrate node) vdom (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE - (Array.zipWith (\node keyedChild → { node, keyedChild }) currentElementChildren keyedChildren) - (\{ keyedChild } → fst keyedChild) + (Array.fromFoldable zippedChildren) + (\{ key } → key) onChild (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.hydrateAttributes currentElement) as1 let diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index e02501d..0240d3d 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -6,14 +6,121 @@ import Data.Array (fromFoldable) as Array import Halogen.VDom.Types (VDom(..)) import Data.List as List import Data.List (List(..), (:)) +import Web.DOM as DOM +import Web.DOM.Element as DOM.Element +import Web.DOM.Node as DOM.Node +import Web.DOM.Text as DOM.Text +import Web.DOM.NodeList as DOM.NodeList +import Web.DOM.Document as DOM.Document +import Web.DOM.CharacterData as DOM.CharacterData +import Data.Maybe (Maybe(..)) +import Control.Alt ((<|>)) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Uncurried as EFn +import Halogen.VDom.Util as Util +import Data.String as String +import Unsafe.Coerce (unsafeCoerce) --- | e.g. --- | [Text ""] -> [] --- | [Text "foo", Text "bar"] -> [Text "foobar"] -normalizeChildren :: forall a w . Array (VDom a w) -> Array (VDom a w) -normalizeChildren = Array.fromFoldable <<< List.foldr go Nil <<< List.fromFoldable - where - go :: VDom a w -> List (VDom a w) -> List (VDom a w) - go (Text "") accum = accum - go (Text text1) (Text text2 : accumt) = Text (text1 <> text2) : accumt - go vdom accum = vdom : accum +data ElementOrTextNode = ElementNode DOM.Element | TextNode DOM.Text + +elementOrTextNodeToNode :: ElementOrTextNode -> DOM.Node +elementOrTextNodeToNode referenceNode = + case referenceNode of + ElementNode element -> DOM.Element.toNode element + TextNode text -> DOM.Text.toNode text + +toElementOrTextNode :: DOM.Node -> Maybe ElementOrTextNode +toElementOrTextNode node = (DOM.Text.fromNode node <#> TextNode) <|> (DOM.Element.fromNode node <#> ElementNode) + +listToElementOrTextNode :: List DOM.Node -> List ElementOrTextNode +listToElementOrTextNode = map toElementOrTextNode >>> List.catMaybes + + +-- | The idea is to prevent rerendering on the next render +-- | but because in the prerendered html all text nodes are merged (`HH.div_ [ HH.text "foo", HH.text "bar" ]` rendered as `
foobar
`, not `
"foo""bar"
`) +-- | and empty text nodes "" are hidden (i.e. they exist in $0.childNodes, but not rendered), +-- | we need to split text nodes using .splitText() and insert "" nodes where it is needed +-- | +-- | check https://jsbin.com/bukulicito/edit?html,output to see how text nodes are added to the parent +-- | +-- | How textNode.splitText() works: +-- | 1. when $0 is '
foobar
' -> $0.childNodes[0].splitText(0) -> returns "foobar", does nothing +-- | 2. when $0 is '
foobar
' -> $0.childNodes[0].splitText(3) -> splits on "foo" and "bar", returns "bar" +-- | 3. when $0 is '
foobar
' -> $0.childNodes[0].splitText(6) -> adds new node "" after "foobar", returns "" +-- | 4. when $0 is '
foobar
' -> $0.childNodes[0].splitText(100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 100 is larger than the Text node's length." +-- | 5. when $0 is '
foobar
' -> $0.childNodes[0].splitText(-100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 4294966996 is larger than the Text node's length." +zipChildrenAndSplitTextNodes + :: forall a w vdomContainer output + . (ElementOrTextNode -> vdomContainer -> output) + -> (vdomContainer -> VDom a w) + -> VDomSpec a w + -> DOM.Node + -> List ElementOrTextNode + -> List vdomContainer + -> Effect (List output) +zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren (vdomChild : vdomChildrenTail) = + let vdomChild' = extractVdom vdomChild + in case domChildren, vdomChild' of + _, Text "" -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } + + (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document + + case domChildren of + -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node + Nil -> void $ DOM.Node.appendChild (DOM.Text.toNode newChildWithEmptyText) parent + -- | when DOM is `
foo
` and vdom is `HH.div_ [HH.text "foo", HH.text ""]` - it wont touch the "foo", but should append new text node "" after "foo" + (referenceNode : _) -> do + let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode + void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent + + let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomChild + + (tailResult :: List output) <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildrenTail + + pure (head : tailResult) + (TextNode textNode : domChildrenTail), (Text expectedText) -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } + textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) + + let expectedTextLength = String.length expectedText + + case compare textNodeLength expectedTextLength of + LT -> do + textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } + throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors + + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate + EQ -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } + + let (head :: output) = toOutput (TextNode textNode) vdomChild + + tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + + pure (head : tailResult) + + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" + GT -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } + nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") + + let (head :: output) = toOutput (TextNode textNode) vdomChild + + tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail + + pure (head : tailResult) + (domChild : domChildrenTail), _ -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} + + let (head :: output) = toOutput domChild vdomChild + + tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + + pure (head : tailResult) + _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" +zipChildrenAndSplitTextNodes toOutput extractVdom spec parent Nil Nil = pure Nil +zipChildrenAndSplitTextNodes toOutput extractVdom spec parent otherDomChildren otherVdomChildren = throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" From fd94726ea7314f6f4c13bd77b8233dfe2fc3c67f Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 17 Jun 2020 18:15:59 +0300 Subject: [PATCH 33/48] refactor: zipChildrenAndSplitTextNodes --- src/Halogen/VDom/DOM/Elem.purs | 1 + src/Halogen/VDom/DOM/Keyed.purs | 1 + src/Halogen/VDom/DOM/Util.purs | 107 ++++++++++++++++---------------- 3 files changed, 57 insertions(+), 52 deletions(-) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index bccc69e..e0daf2d 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -58,6 +58,7 @@ hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 nam let (currentElementChildren' :: List DOMUtil.ElementOrTextNode) = DOMUtil.listToElementOrTextNode currentElementChildren (zippedChildren :: List { node :: DOM.Node, vdom :: VDom a w }) <- + EFn.runEffectFn6 DOMUtil.zipChildrenAndSplitTextNodes (\(node :: DOMUtil.ElementOrTextNode) (vdom :: VDom a w) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom }) identity diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 6dc0dda..64fe744 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -52,6 +52,7 @@ hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 na let (currentElementChildren' :: List DOMUtil.ElementOrTextNode) = DOMUtil.listToElementOrTextNode currentElementChildren (zippedChildren :: List { node :: DOM.Node, vdom :: VDom a w, key :: String }) <- + EFn.runEffectFn6 DOMUtil.zipChildrenAndSplitTextNodes (\(node :: DOMUtil.ElementOrTextNode) (Tuple key vdom) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom, key }) snd diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index 0240d3d..69d038b 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -53,74 +53,77 @@ listToElementOrTextNode = map toElementOrTextNode >>> List.catMaybes -- | 5. when $0 is '
foobar
' -> $0.childNodes[0].splitText(-100) -> throws "Uncaught DOMException: Failed to execute 'splitText' on 'Text': The offset 4294966996 is larger than the Text node's length." zipChildrenAndSplitTextNodes :: forall a w vdomContainer output - . (ElementOrTextNode -> vdomContainer -> output) - -> (vdomContainer -> VDom a w) - -> VDomSpec a w - -> DOM.Node - -> List ElementOrTextNode - -> List vdomContainer - -> Effect (List output) -zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren (vdomChild : vdomChildrenTail) = - let vdomChild' = extractVdom vdomChild - in case domChildren, vdomChild' of - _, Text "" -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } + . EFn.EffectFn6 + (ElementOrTextNode -> vdomContainer -> output) + (vdomContainer -> VDom a w) + (VDomSpec a w) + DOM.Node + (List ElementOrTextNode) + (List vdomContainer) + (List output) +zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildren -> + case domChildren, vdomChildren of + _, (vdomChild : vdomChildrenTail) -> + let vdomChild' = extractVdom vdomChild + in case domChildren, vdomChild' of + _, Text "" -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } - (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document + (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document - case domChildren of - -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node - Nil -> void $ DOM.Node.appendChild (DOM.Text.toNode newChildWithEmptyText) parent - -- | when DOM is `
foo
` and vdom is `HH.div_ [HH.text "foo", HH.text ""]` - it wont touch the "foo", but should append new text node "" after "foo" - (referenceNode : _) -> do - let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode - void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent + case domChildren of + -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node + Nil -> void $ DOM.Node.appendChild (DOM.Text.toNode newChildWithEmptyText) parent + -- | when DOM is `
foo
` and vdom is `HH.div_ [HH.text "foo", HH.text ""]` - it wont touch the "foo", but should append new text node "" after "foo" + (referenceNode : _) -> do + let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode + void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent - let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomChild + let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomChild - (tailResult :: List output) <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildrenTail + (tailResult :: List output) <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildrenTail - pure (head : tailResult) - (TextNode textNode : domChildrenTail), (Text expectedText) -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } - textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) + pure (head : tailResult) + (TextNode textNode : domChildrenTail), (Text expectedText) -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } + textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) - let expectedTextLength = String.length expectedText + let expectedTextLength = String.length expectedText - case compare textNodeLength expectedTextLength of - LT -> do - textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } - throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors + case compare textNodeLength expectedTextLength of + LT -> do + textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } + throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate - EQ -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate + EQ -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } - let (head :: output) = toOutput (TextNode textNode) vdomChild + let (head :: output) = toOutput (TextNode textNode) vdomChild - tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail - pure (head : tailResult) + pure (head : tailResult) - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" - GT -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } - nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" + GT -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } + nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") - let (head :: output) = toOutput (TextNode textNode) vdomChild + let (head :: output) = toOutput (TextNode textNode) vdomChild - tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail - pure (head : tailResult) - (domChild : domChildrenTail), _ -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} + pure (head : tailResult) + (domChild : domChildrenTail), _ -> do + EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} - let (head :: output) = toOutput domChild vdomChild + let (head :: output) = toOutput domChild vdomChild - tailResult <- zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail - pure (head : tailResult) + pure (head : tailResult) + _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" + Nil, Nil -> pure Nil _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" -zipChildrenAndSplitTextNodes toOutput extractVdom spec parent Nil Nil = pure Nil -zipChildrenAndSplitTextNodes toOutput extractVdom spec parent otherDomChildren otherVdomChildren = throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" From 7e60c36248790da2d7e342c9c3757b4769b6d4fc Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 18 Jun 2020 09:32:53 +0300 Subject: [PATCH 34/48] feat: spago --- packages.dhall | 131 +++++++++++++++++++++++++++++++++++++++++++++++++ spago.dhall | 29 +++++++++++ 2 files changed, 160 insertions(+) create mode 100644 packages.dhall create mode 100644 spago.dhall diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..cb13b02 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,131 @@ +{- +Welcome to your new Dhall package-set! + +Below are instructions for how to edit this file for most use +cases, so that you don't need to know Dhall to use it. + +## Warning: Don't Move This Top-Level Comment! + +Due to how `dhall format` currently works, this comment's +instructions cannot appear near corresponding sections below +because `dhall format` will delete the comment. However, +it will not delete a top-level comment like this one. + +## Use Cases + +Most will want to do one or both of these options: +1. Override/Patch a package's dependency +2. Add a package not already in the default package set + +This file will continue to work whether you use one or both options. +Instructions for each option are explained below. + +### Overriding/Patching a package + +Purpose: +- Change a package's dependency to a newer/older release than the + default package set's release +- Use your own modified version of some dependency that may + include new API, changed API, removed API by + using your custom git repo of the library rather than + the package set's repo + +Syntax: +Replace the overrides' "{=}" (an empty record) with the following idea +The "//" or "⫽" means "merge these two records and + when they have the same value, use the one on the right:" +------------------------------- +let overrides = + { packageName = + upstream.packageName // { updateEntity1 = "new value", updateEntity2 = "new value" } + , packageName = + upstream.packageName // { version = "v4.0.0" } + , packageName = + upstream.packageName // { repo = "https://www.example.com/path/to/new/repo.git" } + } +------------------------------- + +Example: +------------------------------- +let overrides = + { halogen = + upstream.halogen // { version = "master" } + , halogen-vdom = + upstream.halogen-vdom // { version = "v4.0.0" } + } +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +Replace the additions' "{=}" (an empty record) with the following idea: +------------------------------- +let additions = + { package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "tag ('v4.0.0') or branch ('master')" + } + , etc. + } +------------------------------- + +Example: +------------------------------- +let additions = + { benchotron = + { dependencies = + [ "arrays" + , "exists" + , "profunctor" + , "strings" + , "quickcheck" + , "lcg" + , "transformers" + , "foldable-traversable" + , "exceptions" + , "node-fs" + , "node-buffer" + , "node-readline" + , "datetime" + , "now" + ] + , repo = + "https://github.com/hdgarrood/purescript-benchotron.git" + , version = + "v7.0.0" + } + } +------------------------------- +-} + + +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200309/packages.dhall sha256:9221987b4e7ea99ccd0efbe056f7bebc872cd92e0058efe5baa181d73359e7b3 + +let overrides = + { web-dom = + upstream.web-dom // { repo = "https://github.com/srghma/purescript-web-dom.git", version = "patch-1" } + } + +let additions = {=} + +in upstream // overrides // additions diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..05a96f8 --- /dev/null +++ b/spago.dhall @@ -0,0 +1,29 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. +-} +{ name = "halogen-vdom" +, dependencies = + [ "bifunctors" + , "console" + , "effect" + , "exists" + , "foreign" + , "foreign-object" + , "js-timers" + , "maybe" + , "prelude" + , "psci-support" + , "refs" + , "tuples" + , "unsafe-coerce" + , "web-html" + , "web-dom" + , "debug" + , "strings" + , "control" + , "lazy" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} From be856962be9c0af3dd79128fdd14b7274bd4c0c8 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Fri, 19 Jun 2020 10:33:37 +0300 Subject: [PATCH 35/48] feat: update comments --- src/Halogen/VDom/DOM/Util.purs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index 69d038b..4534031 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -37,15 +37,15 @@ toElementOrTextNode node = (DOM.Text.fromNode node <#> TextNode) <|> (DOM.Elemen listToElementOrTextNode :: List DOM.Node -> List ElementOrTextNode listToElementOrTextNode = map toElementOrTextNode >>> List.catMaybes - --- | The idea is to prevent rerendering on the next render --- | but because in the prerendered html all text nodes are merged (`HH.div_ [ HH.text "foo", HH.text "bar" ]` rendered as `
foobar
`, not `
"foo""bar"
`) --- | and empty text nodes "" are hidden (i.e. they exist in $0.childNodes, but not rendered), +-- | The idea is to prevent rerendering on the next render after hydration +-- | but because in the prerendered html all Text nodes are merged (i.e. `HH.div_ [ HH.text "foo", HH.text "bar" ]` is rendered as `
foobar
`, not `
"foo""bar"
`) +-- | and empty text nodes "" are hidden (i.e. they exist in $0.childNodes, but are not rendered), -- | we need to split text nodes using .splitText() and insert "" nodes where it is needed -- | --- | check https://jsbin.com/bukulicito/edit?html,output to see how text nodes are added to the parent +-- | Check https://jsbin.com/bukulicito/edit?html,output to see how text nodes are added to the parent +-- | The code was tested on this example https://github.com/srghma/purescript-halogen-nextjs/tree/master/examples/text-nodes -- | --- | How textNode.splitText() works: +-- | Also, to undestand what's happening, you need to understand how textNode.splitText() works: -- | 1. when $0 is '
foobar
' -> $0.childNodes[0].splitText(0) -> returns "foobar", does nothing -- | 2. when $0 is '
foobar
' -> $0.childNodes[0].splitText(3) -> splits on "foo" and "bar", returns "bar" -- | 3. when $0 is '
foobar
' -> $0.childNodes[0].splitText(6) -> adds new node "" after "foobar", returns "" From dcdf4c937595ed692c57405fd955dc7bfa66ea3c Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sun, 21 Jun 2020 18:32:30 +0300 Subject: [PATCH 36/48] feat: add traces --- src/Halogen/VDom/DOM.purs | 16 ++++++++------ src/Halogen/VDom/DOM/Elem.purs | 34 +++++++++++++++++------------ src/Halogen/VDom/DOM/Keyed.purs | 37 ++++++++++++++++++-------------- src/Halogen/VDom/DOM/Text.purs | 18 ++++++++++------ src/Halogen/VDom/DOM/Widget.purs | 10 ++++++--- 5 files changed, 68 insertions(+), 47 deletions(-) diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index fd7d17c..626b096 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -27,7 +27,7 @@ hydrateVDom spec rootNode = hydrate rootNode where build = buildVDom spec hydrate node = EFn.mkEffectFn1 \vdom -> do - EFn.runEffectFn2 warnAny "Path" { node, vdom } + EFn.runEffectFn2 warnAny "hydrate" { 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 @@ -47,9 +47,11 @@ hydrateVDom spec rootNode = hydrate rootNode buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where - build = EFn.mkEffectFn1 case _ of - Text s → EFn.runEffectFn3 buildText spec build s - Elem namespace elemName a childrenVdoms → EFn.runEffectFn6 buildElem spec build namespace elemName a childrenVdoms - Keyed namespace elemName a keyedChildrenVdoms → EFn.runEffectFn6 buildKeyed spec build namespace elemName a keyedChildrenVdoms - Widget w → EFn.runEffectFn3 buildWidget spec build w - Grafted g → EFn.runEffectFn1 build (runGraft g) + build = EFn.mkEffectFn1 \vdom -> do + EFn.runEffectFn2 warnAny "build" { vdom } + case vdom of + Text s → EFn.runEffectFn3 buildText spec build s + Elem namespace elemName a childrenVdoms → EFn.runEffectFn6 buildElem spec build namespace elemName a childrenVdoms + Keyed namespace elemName a keyedChildrenVdoms → EFn.runEffectFn6 buildKeyed spec build namespace elemName a keyedChildrenVdoms + Widget w → EFn.runEffectFn3 buildWidget spec build w + Grafted g → EFn.runEffectFn1 build (runGraft g) diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index e0daf2d..1881d3c 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -2,33 +2,34 @@ module Halogen.VDom.DOM.Elem where import Prelude -import Data.Tuple.Nested (type (/\), (/\)) -import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) -import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) +import Control.Alt ((<|>)) import Data.Array (length, zip, fromFoldable) as Array import Data.Function.Uncurried as Fn +import Data.List (List(..), (:)) +import Data.List as List import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) -import Effect.Uncurried as EFn +import Data.String as String +import Data.Traversable (for) +import Data.Tuple.Nested (type (/\), (/\)) import Effect (Effect) +import Effect.Exception (error, throwException) +import Effect.Uncurried as EFn +import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) +import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util -import Halogen.VDom.DOM.Util as DOMUtil import Unsafe.Coerce (unsafeCoerce) import Web.DOM as DOM +import Web.DOM.CharacterData as DOM.CharacterData +import Web.DOM.Document as DOM.Document import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM.Node -import Web.DOM.Text as DOM.Text import Web.DOM.NodeList as DOM.NodeList -import Web.DOM.Document as DOM.Document -import Web.DOM.CharacterData as DOM.CharacterData -import Data.List (List(..), (:)) -import Data.List as List -import Data.String as String -import Effect.Exception (error, throwException) -import Data.Traversable (for) -import Control.Alt ((<|>)) +import Web.DOM.Text as DOM.Text type ElemState a w = { build ∷ VDomMachine a w @@ -49,6 +50,7 @@ hydrateElem a w hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do + EFn.runEffectFn2 warnAny "hydrateElem" { ns1, name1, as1, ch1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement -- | checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement @@ -93,6 +95,7 @@ buildElem a w buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do + EFn.runEffectFn2 warnAny "buildElem" { ns1, name1, as1, ch1 } el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node @@ -118,6 +121,8 @@ buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do patchElem ∷ ∀ a w. EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) patchElem = EFn.mkEffectFn2 \state vdom → do + EFn.runEffectFn2 warnAny "patchElem" { state, vdom } + let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state case vdom of Grafted g → @@ -175,6 +180,7 @@ patchElem = EFn.mkEffectFn2 \state vdom → do haltElem ∷ ∀ a w. EFn.EffectFn1 (ElemState a w) Unit haltElem = EFn.mkEffectFn1 \{ node, attrs, children } → do + EFn.runEffectFn2 warnAny "haltElem" { node, attrs, children } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent EFn.runEffectFn2 Util.forEachE children halt diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 64fe744..dc6cdf3 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -4,25 +4,26 @@ import Prelude import Data.Array as Array import Data.Function.Uncurried as Fn +import Data.List (List(..), (:)) +import Data.List as List import Data.Maybe (Maybe) import Data.Nullable (toNullable) import Data.Tuple (Tuple(..), fst, snd) import Effect.Uncurried as EFn import Foreign.Object as Object +import Halogen.VDom.DOM.Checkers (checkIsElementNode, checkTagNameIsEqualTo) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) +import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util +import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element -import Web.DOM.NodeList as DOM.NodeList -import Unsafe.Coerce (unsafeCoerce) import Web.DOM.Node (Node, childNodes) as DOM -import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) -import Halogen.VDom.DOM.Checkers (checkIsElementNode, checkTagNameIsEqualTo) -import Data.List (List(..), (:)) -import Data.List as List -import Halogen.VDom.DOM.Util as DOMUtil +import Web.DOM.NodeList as DOM.NodeList type KeyedState a w = { build ∷ VDomMachine a w @@ -43,7 +44,8 @@ hydrateKeyed (Array (Tuple String (VDom a w))) a w -hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren → do +hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren1 → do + EFn.runEffectFn2 warnAny "hydrateKeyed" { ns1, name1, as1, keyedChildren1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement @@ -59,7 +61,7 @@ hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 na (VDomSpec spec) currentNode currentElementChildren' - (List.fromFoldable keyedChildren) + (List.fromFoldable keyedChildren1) let onChild :: EFn.EffectFn3 String Int ({ node :: DOM.Node, vdom :: VDom a w, key :: String }) (Step (VDom a w) DOM.Node) @@ -79,12 +81,13 @@ hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 na , ns: ns1 , name: name1 , children - , length: Array.length keyedChildren + , length: Array.length keyedChildren1 } pure $ mkStep $ Step currentNode state patchKeyed haltKeyed buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w -buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do +buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 keyedChildren1 → do + EFn.runEffectFn2 warnAny "buildKeyed" { ns1, name1, as1, keyedChildren1 } el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node @@ -95,7 +98,7 @@ buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do res ← EFn.runEffectFn1 build vdom EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res - (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE ch1 fst onChild -- build keyed childrens + (children :: Object.Object (Step (VDom a w) DOM.Node)) ← EFn.runEffectFn3 Util.strMapWithIxE keyedChildren1 fst onChild -- build keyed childrens (attrs :: Step a Unit) ← EFn.runEffectFn1 (spec.buildAttributes el) as1 let state = @@ -105,13 +108,14 @@ buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do , ns: ns1 , name: name1 , children - , length: Array.length ch1 + , length: Array.length keyedChildren1 } pure $ mkStep $ Step node state patchKeyed haltKeyed patchKeyed ∷ ∀ a w. EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) patchKeyed = EFn.mkEffectFn2 \state vdom → do - let { build, node, attrs, ns: ns1, name: name1, children: ch1, length: len1 } = state + EFn.runEffectFn2 warnAny "patchKeyed" { state, vdom } + let { build, node, attrs, ns: ns1, name: name1, children: keyedChildren1, length: len1 } = state case vdom of Grafted g → EFn.runEffectFn2 patchKeyed state (runGraft g) @@ -126,7 +130,7 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do , attrs: attrs2 , ns: ns2 , name: name2 - , children: ch1 + , children: keyedChildren1 , length: 0 } pure $ mkStep $ Step node nextState patchKeyed haltKeyed @@ -146,7 +150,7 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do res ← EFn.runEffectFn1 build v EFn.runEffectFn3 Util.insertChildIx ix (extract res) node pure res - children2 ← EFn.runEffectFn6 Util.diffWithKeyAndIxE ch1 ch2 fst onThese onThis onThat + children2 ← EFn.runEffectFn6 Util.diffWithKeyAndIxE keyedChildren1 ch2 fst onThese onThis onThat attrs2 ← EFn.runEffectFn2 step attrs as2 let nextState = @@ -165,6 +169,7 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do haltKeyed ∷ ∀ a w. EFn.EffectFn1 (KeyedState a w) Unit haltKeyed = EFn.mkEffectFn1 \{ node, attrs, children } → do + EFn.runEffectFn2 warnAny "haltKeyed" { node, attrs, children } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent EFn.runEffectFn2 Util.forInE children (EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s) diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index 15078db..6b8f7f0 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -1,13 +1,13 @@ module Halogen.VDom.DOM.Text where -import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomStep) -import Halogen.VDom.DOM.Checkers (checkIsTextNode, checkTextContentIsEqTo) -import Prelude (Unit, bind, discard, otherwise, pure, ($), (==)) - import Effect.Uncurried as EFn +import Halogen.VDom.DOM.Checkers (checkIsTextNode, checkTextContentIsEqTo) +import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomStep) import Halogen.VDom.Machine (Step'(..), mkStep) import Halogen.VDom.Types (VDom(..), runGraft) +import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util +import Prelude (Unit, bind, discard, otherwise, pure, ($), (==)) import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM @@ -20,6 +20,7 @@ type TextState a w = -- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → do + EFn.runEffectFn2 warnAny "hydrateText" { s } currentText <- checkIsTextNode currentNode checkTextContentIsEqTo s currentText let (state :: TextState a w) = { build, node: currentNode, value: s } @@ -27,14 +28,16 @@ hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → buildText ∷ ∀ a w. VDomBuilder String a w buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do + EFn.runEffectFn2 warnAny "buildText" { s } node ← EFn.runEffectFn2 Util.createTextNode s spec.document let (state :: TextState a w) = { build, node, value: s } pure $ mkStep $ Step node state patchText haltText patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) -patchText = EFn.mkEffectFn2 \state newVdom → do +patchText = EFn.mkEffectFn2 \state vdom → do + EFn.runEffectFn2 warnAny "patchText" { state, vdom } let { build, node, value: value1 } = state - case newVdom of + case vdom of Grafted g → EFn.runEffectFn2 patchText state (runGraft g) -- Before there was a Text on this place. We call patchText instead of patch to be able to remove text Text value2 @@ -46,9 +49,10 @@ patchText = EFn.mkEffectFn2 \state newVdom → do pure $ mkStep $ Step node nextState patchText haltText _ → do EFn.runEffectFn1 haltText state - EFn.runEffectFn1 build newVdom + EFn.runEffectFn1 build vdom haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit haltText = EFn.mkEffectFn1 \{ node } → do + EFn.runEffectFn2 warnAny "haltText" { node } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index 539622a..880d451 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -1,12 +1,12 @@ module Halogen.VDom.DOM.Widget where -import Prelude (Unit, bind, discard, pure, (#), ($)) - import Effect.Uncurried as EFn +import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep, VDomHydrator) import Halogen.VDom.Machine (Step, Step'(..), halt, mkStep, step, unStep) import Halogen.VDom.Types (VDom(..), runGraft) +import Halogen.VDom.Util (warnAny) +import Prelude (Unit, bind, discard, pure, (#), ($)) import Web.DOM.Node (Node) as DOM -import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep, VDomHydrator) type WidgetState a w = { build ∷ VDomMachine a w @@ -15,6 +15,7 @@ type WidgetState a w = hydrateWidget ∷ ∀ a w. VDomHydrator w a w hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpec spec) _hydrate build w → do + EFn.runEffectFn2 warnAny "hydrateWidget" { w } res ← EFn.runEffectFn1 (spec.hydrateWidget (VDomSpec spec) elem) w let res' :: Step (VDom a w) DOM.Node @@ -24,6 +25,7 @@ hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpec spec) _hydrate build w → do buildWidget ∷ ∀ a w. VDomBuilder w a w buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do + EFn.runEffectFn2 warnAny "buildWidget" { w } res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w let res' :: Step (VDom a w) DOM.Node @@ -33,6 +35,7 @@ buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do patchWidget ∷ ∀ a w. EFn.EffectFn2 (WidgetState a w) (VDom a w) (VDomStep a w) patchWidget = EFn.mkEffectFn2 \state vdom → do + EFn.runEffectFn2 warnAny "patchWidget" { state, vdom } let { build, widget } = state case vdom of Grafted g → @@ -49,4 +52,5 @@ patchWidget = EFn.mkEffectFn2 \state vdom → do haltWidget ∷ forall a w. EFn.EffectFn1 (WidgetState a w) Unit haltWidget = EFn.mkEffectFn1 \{ widget } → do + EFn.runEffectFn2 warnAny "haltWidget" { widget } EFn.runEffectFn1 halt widget From 953ba6659b9b7ed22341539c7f8121b7fa269904 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 24 Jun 2020 19:38:22 +0300 Subject: [PATCH 37/48] feat: split VDomSpec on VDomSpec and VDomSpecWithHydration --- src/Halogen/VDom.purs | 2 +- src/Halogen/VDom/DOM.purs | 29 ++++++++++++++--------------- src/Halogen/VDom/DOM/Elem.purs | 6 +++--- src/Halogen/VDom/DOM/Keyed.purs | 6 +++--- src/Halogen/VDom/DOM/Text.purs | 5 ++--- src/Halogen/VDom/DOM/Types.purs | 15 ++++++++------- src/Halogen/VDom/DOM/Util.purs | 14 +++++++------- src/Halogen/VDom/DOM/Widget.purs | 6 +++--- src/Halogen/VDom/Thunk.purs | 2 +- src/Halogen/VDom/Util.purs | 7 +++---- test/Hydration.purs | 5 +++-- test/TestVdom.purs | 11 +++++++++-- 12 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/Halogen/VDom.purs b/src/Halogen/VDom.purs index 18691f8..1261cce 100644 --- a/src/Halogen/VDom.purs +++ b/src/Halogen/VDom.purs @@ -4,6 +4,6 @@ module Halogen.VDom , module Types ) where -import Halogen.VDom.DOM (VDomSpec(..), buildVDom, hydrateVDom) as DOM +import Halogen.VDom.DOM (VDomSpec(..), VDomSpecWithHydration(..), buildVDom, hydrateVDom) as DOM import Halogen.VDom.Machine (Machine, Step, Step'(..), mkStep, unStep, extract, step, halt) as Machine import Halogen.VDom.Types (VDom(..), Graft, runGraft, ElemName(..), Namespace(..)) as Types diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 626b096..6a6e991 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -5,34 +5,33 @@ module Halogen.VDom.DOM ) 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 +import Halogen.VDom.DOM.Elem (buildElem, hydrateElem) import Halogen.VDom.DOM.Keyed (buildKeyed) as Export +import Halogen.VDom.DOM.Keyed (buildKeyed, hydrateKeyed) import Halogen.VDom.DOM.Text (buildText) as Export -import Halogen.VDom.DOM.Types (VDomSpec(..)) as Export +import Halogen.VDom.DOM.Text (buildText, hydrateText) +import Halogen.VDom.DOM.Types (VDomMachine, VDomSpec, VDomSpecWithHydration(..)) +import Halogen.VDom.DOM.Types (VDomSpec(..), VDomSpecWithHydration(..)) as Export import Halogen.VDom.DOM.Widget (buildWidget) as Export +import Halogen.VDom.DOM.Widget (buildWidget, hydrateWidget) import Halogen.VDom.Types (VDom(..), runGraft) -import Web.DOM.Element (Element) as DOM +import Halogen.VDom.Util (warnAny) import Web.DOM.Node (Node) as DOM -hydrateVDom ∷ ∀ a w. VDomSpec a w → DOM.Node -> VDomMachine a w -hydrateVDom spec rootNode = hydrate rootNode +hydrateVDom ∷ ∀ a w. VDomSpecWithHydration a w → DOM.Node -> VDomMachine a w +hydrateVDom specWithHydration@(VDomSpecWithHydration specWithHydration') rootNode = hydrate rootNode where - build = buildVDom spec + build = buildVDom specWithHydration'.vdomSpec hydrate node = EFn.mkEffectFn1 \vdom -> do EFn.runEffectFn2 warnAny "hydrate" { 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 - Keyed namespace elemName attribute keyedChildrenVdoms → EFn.runEffectFn8 hydrateKeyed node spec hydrate build namespace elemName attribute keyedChildrenVdoms - Widget w → EFn.runEffectFn5 hydrateWidget node spec hydrate build w + Text s → EFn.runEffectFn5 hydrateText node specWithHydration hydrate build s + Elem namespace elemName attribute childrenVdoms → EFn.runEffectFn8 hydrateElem node specWithHydration hydrate build namespace elemName attribute childrenVdoms + Keyed namespace elemName attribute keyedChildrenVdoms → EFn.runEffectFn8 hydrateKeyed node specWithHydration hydrate build namespace elemName attribute keyedChildrenVdoms + Widget w → EFn.runEffectFn5 hydrateWidget node specWithHydration hydrate build w Grafted g → EFn.runEffectFn1 (hydrate node) (runGraft g) -- | Starts an initial `VDom` machine by providing a `VDomSpec`. diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 1881d3c..266ba37 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -16,7 +16,7 @@ import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) -import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep) import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) @@ -49,7 +49,7 @@ hydrateElem (Array (VDom a w)) a w -hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 ch1 → do +hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpecWithHydration spec) hydrate build ns1 name1 as1 ch1 → do EFn.runEffectFn2 warnAny "hydrateElem" { ns1, name1, as1, ch1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement @@ -64,7 +64,7 @@ hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 nam DOMUtil.zipChildrenAndSplitTextNodes (\(node :: DOMUtil.ElementOrTextNode) (vdom :: VDom a w) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom }) identity - (VDomSpec spec) + (case spec.vdomSpec of VDomSpec vdomSpec -> vdomSpec).document currentNode currentElementChildren' (List.fromFoldable ch1) diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index dc6cdf3..504e7c1 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -12,7 +12,7 @@ import Data.Tuple (Tuple(..), fst, snd) import Effect.Uncurried as EFn import Foreign.Object as Object import Halogen.VDom.DOM.Checkers (checkIsElementNode, checkTagNameIsEqualTo) -import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep) import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Machine as Machine @@ -44,7 +44,7 @@ hydrateKeyed (Array (Tuple String (VDom a w))) a w -hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 name1 as1 keyedChildren1 → do +hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpecWithHydration spec) hydrate build ns1 name1 as1 keyedChildren1 → do EFn.runEffectFn2 warnAny "hydrateKeyed" { ns1, name1, as1, keyedChildren1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement @@ -58,7 +58,7 @@ hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpec spec) hydrate build ns1 na DOMUtil.zipChildrenAndSplitTextNodes (\(node :: DOMUtil.ElementOrTextNode) (Tuple key vdom) -> { node: DOMUtil.elementOrTextNodeToNode node, vdom, key }) snd - (VDomSpec spec) + (case spec.vdomSpec of VDomSpec vdomSpec -> vdomSpec).document currentNode currentElementChildren' (List.fromFoldable keyedChildren1) diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index 6b8f7f0..a11ddf1 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -2,13 +2,12 @@ module Halogen.VDom.DOM.Text where import Effect.Uncurried as EFn import Halogen.VDom.DOM.Checkers (checkIsTextNode, checkTextContentIsEqTo) -import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomStep) +import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep) import Halogen.VDom.Machine (Step'(..), mkStep) import Halogen.VDom.Types (VDom(..), runGraft) import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util import Prelude (Unit, bind, discard, otherwise, pure, ($), (==)) -import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node) as DOM type TextState a w = @@ -19,7 +18,7 @@ type TextState a w = -- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w -hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpec spec) _hydrate build s → do +hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpecWithHydration spec) _hydrate build s → do EFn.runEffectFn2 warnAny "hydrateText" { s } currentText <- checkIsTextNode currentNode checkTextContentIsEqTo s currentText diff --git a/src/Halogen/VDom/DOM/Types.purs b/src/Halogen/VDom/DOM/Types.purs index c92b13c..e7a9a7a 100644 --- a/src/Halogen/VDom/DOM/Types.purs +++ b/src/Halogen/VDom/DOM/Types.purs @@ -20,7 +20,7 @@ type VDomBuilder i a w = EFn.EffectFn3 (VDomSpec a w) (VDomMachine a w) i (VDomS type VDomHydrator i a w = EFn.EffectFn5 DOM.Node -- current element - (VDomSpec a w) + (VDomSpecWithHydration a w) (DOM.Node -> VDomMachine a w) -- top hydrate function (VDomMachine a w) -- top build function i @@ -31,7 +31,7 @@ type VDomBuilder4 i j k l a w = EFn.EffectFn6 (VDomSpec a w) (VDomMachine a w) i type VDomHydrator4 i j k l a w = EFn.EffectFn8 DOM.Node - (VDomSpec a w) + (VDomSpecWithHydration a w) (DOM.Node -> VDomMachine a w) (VDomMachine a w) i @@ -44,11 +44,12 @@ type VDomHydrator4 i j k l a w -- | enable recursive trees of Widgets. newtype VDomSpec a w = VDomSpec { buildWidget ∷ VDomSpec a w → Machine w DOM.Node -- `buildWidget` takes a circular reference to the `VDomSpec` - , hydrateWidget ∷ VDomSpec a w → DOM.Node → Machine w DOM.Node - , buildAttributes ∷ DOM.Element → Machine a Unit - , hydrateAttributes ∷ DOM.Element → Machine a Unit + , document ∷ DOM.Document -- We need document to be able to call `document.createElement` function + } - -- We need document to be able to call `document.createElement` function - , document ∷ DOM.Document +newtype VDomSpecWithHydration a w = VDomSpecWithHydration + { vdomSpec ∷ VDomSpec a w + , hydrateWidget ∷ VDomSpecWithHydration a w → DOM.Node → Machine w DOM.Node + , hydrateAttributes ∷ DOM.Element → Machine a Unit } diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index 4534031..b63fcbf 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -56,12 +56,12 @@ zipChildrenAndSplitTextNodes . EFn.EffectFn6 (ElementOrTextNode -> vdomContainer -> output) (vdomContainer -> VDom a w) - (VDomSpec a w) + DOM.Document DOM.Node (List ElementOrTextNode) (List vdomContainer) (List output) -zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildren -> +zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom document parent domChildren vdomChildren -> case domChildren, vdomChildren of _, (vdomChild : vdomChildrenTail) -> let vdomChild' = extractVdom vdomChild @@ -69,7 +69,7 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec s _, Text "" -> do EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } - (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" spec.document + (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" document case domChildren of -- | when DOM is `
` (no children) and vdom is `HH.div_ [HH.text ""]` - it will create append new text node @@ -81,7 +81,7 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec s let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomChild - (tailResult :: List output) <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildren vdomChildrenTail + (tailResult :: List output) <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildren vdomChildrenTail pure (head : tailResult) (TextNode textNode : domChildrenTail), (Text expectedText) -> do @@ -102,7 +102,7 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec s let (head :: output) = toOutput (TextNode textNode) vdomChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomChildrenTail pure (head : tailResult) @@ -113,7 +113,7 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec s let (head :: output) = toOutput (TextNode textNode) vdomChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail pure (head : tailResult) (domChild : domChildrenTail), _ -> do @@ -121,7 +121,7 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom (VDomSpec s let (head :: output) = toOutput domChild vdomChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom (VDomSpec spec) parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomChildrenTail pure (head : tailResult) _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index 880d451..d3650d7 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -1,7 +1,7 @@ module Halogen.VDom.DOM.Widget where import Effect.Uncurried as EFn -import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomStep, VDomHydrator) +import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep, VDomHydrator) import Halogen.VDom.Machine (Step, Step'(..), halt, mkStep, step, unStep) import Halogen.VDom.Types (VDom(..), runGraft) import Halogen.VDom.Util (warnAny) @@ -14,9 +14,9 @@ type WidgetState a w = } hydrateWidget ∷ ∀ a w. VDomHydrator w a w -hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpec spec) _hydrate build w → do +hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpecWithHydration spec) _hydrate build w → do EFn.runEffectFn2 warnAny "hydrateWidget" { w } - res ← EFn.runEffectFn1 (spec.hydrateWidget (VDomSpec spec) elem) w + res ← EFn.runEffectFn1 (spec.hydrateWidget (VDomSpecWithHydration spec) elem) w let res' :: Step (VDom a w) DOM.Node res' = res # unStep \(Step n s k1 k2) → diff --git a/src/Halogen/VDom/Thunk.purs b/src/Halogen/VDom/Thunk.purs index 3825291..8d7d067 100644 --- a/src/Halogen/VDom/Thunk.purs +++ b/src/Halogen/VDom/Thunk.purs @@ -101,7 +101,7 @@ type ThunkState f i a w = hydrateThunk ∷ ∀ f i a w . (f i → V.VDom a w) - → V.VDomSpec a w + → V.VDomSpecWithHydration a w → Node → V.Machine (Thunk f i) Node hydrateThunk toVDom spec element = mkThunkBuilder (V.hydrateVDom spec element) toVDom diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index f7a21d5..6063914 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -16,7 +16,6 @@ import Web.DOM.Element (Element) as DOM import Web.DOM.Node (Node) as DOM import Web.Event.EventTarget (EventListener) as DOM import Data.Maybe (Maybe(..)) -import Data.Newtype (unwrap) data STObject' a -- just like STObject, but without region @@ -151,10 +150,10 @@ foreign import warnAny ∷ ∀ a . EFn.EffectFn2 String a Unit foreign import logAny ∷ ∀ a . EFn.EffectFn2 String a Unit fullAttributeName ∷ Maybe Namespace → ElemName → String -fullAttributeName maybeNamespace elemName = +fullAttributeName maybeNamespace (ElemName elemName) = case maybeNamespace of - Just namespace -> unwrap namespace <> ":" <> unwrap elemName - Nothing -> unwrap elemName + Just (Namespace namespace) -> namespace <> ":" <> elemName + Nothing -> elemName eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → diff --git a/test/Hydration.purs b/test/Hydration.purs index 1776a75..7ad444e 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -16,7 +16,7 @@ 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 Test.TestVdom (VDom(..), elem, keyed, mkSpecWithHydration, text, thunk, (:=)) import Web.Event.EventTarget (eventListener) as DOM import Web.HTML (window) as DOM import Web.HTML.HTMLDocument (toDocument, toParentNode) as DOM @@ -64,10 +64,11 @@ main = do updateStateButton ← findRequiredElement "#update-state-button" (DOM.toParentNode doc) let - spec = mkSpec (DOM.toDocument doc) + spec = mkSpecWithHydration (DOM.toDocument doc) initialValue = initialState render = renderData initialVdom = un VDom (render initialValue) + machine ← EFn.runEffectFn1 (V.hydrateVDom spec (DOM.Element.toNode rootElement)) initialVdom listener ← DOM.eventListener \_ev → diff --git a/test/TestVdom.purs b/test/TestVdom.purs index f2d1dac..7d023e2 100644 --- a/test/TestVdom.purs +++ b/test/TestVdom.purs @@ -48,8 +48,15 @@ mkSpec → V.VDomSpec (Array (Prop Void)) (Thunk VDom Void) mkSpec document = V.VDomSpec { buildWidget: buildThunk (un VDom) - , hydrateWidget: hydrateThunk (un VDom) , buildAttributes: buildProp (const (pure unit)) - , hydrateAttributes: hydrateProp (const (pure unit)) , document } + +mkSpecWithHydration + ∷ DOM.Document + → V.VDomSpecWithHydration (Array (Prop Void)) (Thunk VDom Void) +mkSpecWithHydration document = V.VDomSpecWithHydration + { vdomSpec: mkSpec document + , hydrateWidget: hydrateThunk (un VDom) + , hydrateAttributes: hydrateProp (const (pure unit)) + } From 4a1ac34ceb4a9fc5567db4c9650bc8952bfbc470 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 24 Jun 2020 21:21:34 +0300 Subject: [PATCH 38/48] feat: remove debug, improve errors --- HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md | 21 +--- README.md | 3 - src/Halogen/VDom/DOM.purs | 5 - src/Halogen/VDom/DOM/Checkers.purs | 35 ++---- src/Halogen/VDom/DOM/Elem.purs | 23 +--- src/Halogen/VDom/DOM/Keyed.purs | 9 +- src/Halogen/VDom/DOM/Prop/Checkers.purs | 33 +++-- src/Halogen/VDom/DOM/Prop/Implementation.purs | 31 +++-- src/Halogen/VDom/DOM/Text.purs | 6 - src/Halogen/VDom/DOM/Util.purs | 64 +++++----- src/Halogen/VDom/DOM/Widget.purs | 5 - src/Halogen/VDom/Types.purs | 2 +- test/Hydration.purs | 119 +++++++++--------- 13 files changed, 142 insertions(+), 214 deletions(-) diff --git a/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md b/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md index 0aa7733..93dd8f2 100644 --- a/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md +++ b/HOW_DOM_ATTRIBUTES_AND_PROPERTIES_WORK.md @@ -1,4 +1,4 @@ -# how .attributes work +# How .attributes work `$0.attributes` are: - is { required: "" } @@ -6,7 +6,7 @@ - is { colspan: "1" }, but prop is colSpan = 1 -
is { "foo:data-foo": "1" } -# properties +# Properties ``` Having `` @@ -22,7 +22,7 @@ If do `$0.required = false` in chrome. THE $0.attributes = {} ``` -# how .dataset property works +# How .dataset property works? should we support it? ``` Having `
` @@ -39,18 +39,3 @@ Also, react doesnt support dataset property during hydration. Proof: - errorMessage = `Prop `dataset` did not match. Server: "null" Client: "[object Object]"` thus, react does support `data-***` attributes, but doesn't support `dataset` property, so why bother with supporting `dataset` property? - -But, in future, If we want to support dataset, then we sould implment something like - -```purs -data PropValue = PropValue_String String | PropValue_Int Int | ... | PropValue_Dataset (Object String) -removePropFromExtraAttributeNames ∷ PropName → PropValue → Set → Set -removePropFromExtraAttributeNames propName propValue set = - if propName == "dataset" - then forEach propValue - (\key _val → do - remove ("data-" <> camelCaseToKebabCase key) set - ) - else do - remove (camelCaseToKebabCase key) set -``` diff --git a/README.md b/README.md index 76e51a8..4694090 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,3 @@ implementation included). It is intended to be extended (and likely * Read the [guide](./GUIDE.md). * See the [test example](./test/Main.purs). - -TODO: -- use https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.server.js#L196-L201 diff --git a/src/Halogen/VDom/DOM.purs b/src/Halogen/VDom/DOM.purs index 6a6e991..6e21b4e 100644 --- a/src/Halogen/VDom/DOM.purs +++ b/src/Halogen/VDom/DOM.purs @@ -4,8 +4,6 @@ module Halogen.VDom.DOM , hydrateVDom ) where -import Prelude - import Effect.Uncurried as EFn import Halogen.VDom.DOM.Elem (buildElem) as Export import Halogen.VDom.DOM.Elem (buildElem, hydrateElem) @@ -18,7 +16,6 @@ import Halogen.VDom.DOM.Types (VDomSpec(..), VDomSpecWithHydration(..)) as Expor import Halogen.VDom.DOM.Widget (buildWidget) as Export import Halogen.VDom.DOM.Widget (buildWidget, hydrateWidget) import Halogen.VDom.Types (VDom(..), runGraft) -import Halogen.VDom.Util (warnAny) import Web.DOM.Node (Node) as DOM hydrateVDom ∷ ∀ a w. VDomSpecWithHydration a w → DOM.Node -> VDomMachine a w @@ -26,7 +23,6 @@ hydrateVDom specWithHydration@(VDomSpecWithHydration specWithHydration') rootNod where build = buildVDom specWithHydration'.vdomSpec hydrate node = EFn.mkEffectFn1 \vdom -> do - EFn.runEffectFn2 warnAny "hydrate" { node, vdom } case vdom of Text s → EFn.runEffectFn5 hydrateText node specWithHydration hydrate build s Elem namespace elemName attribute childrenVdoms → EFn.runEffectFn8 hydrateElem node specWithHydration hydrate build namespace elemName attribute childrenVdoms @@ -47,7 +43,6 @@ buildVDom ∷ ∀ a w. VDomSpec a w → VDomMachine a w buildVDom spec = build where build = EFn.mkEffectFn1 \vdom -> do - EFn.runEffectFn2 warnAny "build" { vdom } case vdom of Text s → EFn.runEffectFn3 buildText spec build s Elem namespace elemName a childrenVdoms → EFn.runEffectFn6 buildElem spec build namespace elemName a childrenVdoms diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 193512b..bd7aa28 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -13,39 +13,36 @@ import Partial.Unsafe (unsafePartial) import Web.DOM as DOM import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM.Node -import Web.DOM.NodeList as DOM.NodeList import Web.DOM.NodeType as DOM.NodeType import Web.DOM.Text as DOM.Text import Web.DOM.CharacterData as DOM.CharacterData --------------------------------------- --- Text +-- | Text checkers checkIsTextNode :: DOM.Node -> Effect DOM.Text checkIsTextNode node = case DOM.Text.fromNode node of Just text -> pure text Nothing -> do - EFn.runEffectFn2 Util.warnAny "Error at " { node } - throwException $ error $ "Expected node to be a " <> show DOM.NodeType.TextNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) + EFn.runEffectFn2 Util.warnAny "Error info: " { node } + throwException $ error $ "Expected node to be a " <> show DOM.NodeType.TextNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) <> " (check warning above for more information)" checkTextContentIsEqTo :: String -> DOM.Text -> Effect Unit checkTextContentIsEqTo expectedText text = do textContent <- DOM.CharacterData.data_ (DOM.Text.toCharacterData text) - EFn.runEffectFn2 Util.warnAny "checkTextContentIsEqTo" { textContent, expectedText, meta: { text } } - when (textContent /= expectedText) (do - throwException $ error $ "Expected element text content to equal to " <> Util.quote expectedText <> ", but got " <> Util.quote textContent) + when (textContent /= expectedText) do + EFn.runEffectFn2 Util.warnAny "Error info: " { text } + throwException $ error $ "Expected element text content to equal to " <> Util.quote expectedText <> ", but got " <> Util.quote textContent <> " (check warning above for more information)" --------------------------------------- --- Elem +-- | Elem checkers checkIsElementNode :: DOM.Node -> Effect DOM.Element checkIsElementNode node = case DOM.Element.fromNode node of Just text -> pure text Nothing -> do - EFn.runEffectFn2 Util.warnAny "Error at " { node } - throwException $ error $ "Expected node to be a " <> show DOM.NodeType.ElementNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) + EFn.runEffectFn2 Util.warnAny "Error info: " { node } + throwException $ error $ "Expected node to be a " <> show DOM.NodeType.ElementNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) <> " (check warning above for more information)" checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit checkTagNameIsEqualTo maybeNamespace elemName element = do @@ -54,14 +51,6 @@ checkTagNameIsEqualTo maybeNamespace elemName element = do expectedTagName :: String expectedTagName = toUpper $ Util.fullAttributeName maybeNamespace elemName let tagName = DOM.Element.tagName element - EFn.runEffectFn2 Util.warnAny "checkTagNameIsEqualTo" { expectedTagName, tagName, meta: { maybeNamespace, elemName, element } } - when (tagName /= expectedTagName) (do - throwException (error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName)) - -checkChildrenLengthIsEqualTo :: Int -> DOM.Element -> Effect Unit -checkChildrenLengthIsEqualTo expectedLength element = do - (elementChildren :: DOM.NodeList) <- DOM.Node.childNodes (DOM.Element.toNode element) - elementChildrenLength <- DOM.NodeList.length elementChildren - EFn.runEffectFn2 Util.warnAny "checkChildrenLengthIsEqualTo" { elementChildrenLength, expectedLength, meta: { element, elementChildren } } - when (elementChildrenLength /= expectedLength) do - (throwException (error $ "Expected element children count equal to " <> show expectedLength <> ", but got " <> show elementChildrenLength)) + when (tagName /= expectedTagName) do + EFn.runEffectFn2 Util.warnAny "Error info: " { maybeNamespace, elemName, element } + throwException $ error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName <> " (check warning above for more information)" diff --git a/src/Halogen/VDom/DOM/Elem.purs b/src/Halogen/VDom/DOM/Elem.purs index 266ba37..32c2e62 100644 --- a/src/Halogen/VDom/DOM/Elem.purs +++ b/src/Halogen/VDom/DOM/Elem.purs @@ -2,34 +2,25 @@ module Halogen.VDom.DOM.Elem where import Prelude -import Control.Alt ((<|>)) -import Data.Array (length, zip, fromFoldable) as Array +import Data.Array (fromFoldable, length) as Array import Data.Function.Uncurried as Fn -import Data.List (List(..), (:)) +import Data.List (List) import Data.List as List -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Data.Nullable (toNullable) -import Data.String as String import Data.Traversable (for) -import Data.Tuple.Nested (type (/\), (/\)) import Effect (Effect) -import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Halogen.VDom.DOM.Checkers (checkChildrenLengthIsEqualTo, checkIsElementNode, checkTagNameIsEqualTo) +import Halogen.VDom.DOM.Checkers (checkIsElementNode, checkTagNameIsEqualTo) import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep) import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) -import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util -import Unsafe.Coerce (unsafeCoerce) import Web.DOM as DOM -import Web.DOM.CharacterData as DOM.CharacterData -import Web.DOM.Document as DOM.Document import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM.Node import Web.DOM.NodeList as DOM.NodeList -import Web.DOM.Text as DOM.Text type ElemState a w = { build ∷ VDomMachine a w @@ -50,10 +41,8 @@ hydrateElem a w hydrateElem = EFn.mkEffectFn8 \currentNode (VDomSpecWithHydration spec) hydrate build ns1 name1 as1 ch1 → do - EFn.runEffectFn2 warnAny "hydrateElem" { ns1, name1, as1, ch1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement - -- | checkChildrenLengthIsEqualTo (Array.length normalizedChildren) currentElement (currentElementChildren :: List DOM.Node) <- DOM.Node.childNodes currentNode >>= DOM.NodeList.toArray <#> List.fromFoldable @@ -95,7 +84,6 @@ buildElem a w buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do - EFn.runEffectFn2 warnAny "buildElem" { ns1, name1, as1, ch1 } el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node @@ -121,8 +109,6 @@ buildElem = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 ch1 → do patchElem ∷ ∀ a w. EFn.EffectFn2 (ElemState a w) (VDom a w) (VDomStep a w) patchElem = EFn.mkEffectFn2 \state vdom → do - EFn.runEffectFn2 warnAny "patchElem" { state, vdom } - let { build, node, attrs, ns: ns1, name: name1, children: ch1 } = state case vdom of Grafted g → @@ -180,7 +166,6 @@ patchElem = EFn.mkEffectFn2 \state vdom → do haltElem ∷ ∀ a w. EFn.EffectFn1 (ElemState a w) Unit haltElem = EFn.mkEffectFn1 \{ node, attrs, children } → do - EFn.runEffectFn2 warnAny "haltElem" { node, attrs, children } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent EFn.runEffectFn2 Util.forEachE children halt diff --git a/src/Halogen/VDom/DOM/Keyed.purs b/src/Halogen/VDom/DOM/Keyed.purs index 504e7c1..fa00bec 100644 --- a/src/Halogen/VDom/DOM/Keyed.purs +++ b/src/Halogen/VDom/DOM/Keyed.purs @@ -4,7 +4,7 @@ import Prelude import Data.Array as Array import Data.Function.Uncurried as Fn -import Data.List (List(..), (:)) +import Data.List (List) import Data.List as List import Data.Maybe (Maybe) import Data.Nullable (toNullable) @@ -17,10 +17,7 @@ import Halogen.VDom.DOM.Util as DOMUtil import Halogen.VDom.Machine (Step, Step'(..), extract, halt, mkStep, step) import Halogen.VDom.Machine as Machine import Halogen.VDom.Types (ElemName, Namespace, VDom(..), runGraft) -import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util -import Unsafe.Coerce (unsafeCoerce) -import Web.DOM.Element (Element) as DOM import Web.DOM.Element as DOM.Element import Web.DOM.Node (Node, childNodes) as DOM import Web.DOM.NodeList as DOM.NodeList @@ -45,7 +42,6 @@ hydrateKeyed a w hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpecWithHydration spec) hydrate build ns1 name1 as1 keyedChildren1 → do - EFn.runEffectFn2 warnAny "hydrateKeyed" { ns1, name1, as1, keyedChildren1 } currentElement <- checkIsElementNode currentNode checkTagNameIsEqualTo ns1 name1 currentElement @@ -87,7 +83,6 @@ hydrateKeyed = EFn.mkEffectFn8 \currentNode (VDomSpecWithHydration spec) hydrate buildKeyed ∷ ∀ a w. VDomBuilder4 (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) a w buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 keyedChildren1 → do - EFn.runEffectFn2 warnAny "buildKeyed" { ns1, name1, as1, keyedChildren1 } el ← EFn.runEffectFn3 Util.createElement (toNullable ns1) name1 spec.document let node :: DOM.Node @@ -114,7 +109,6 @@ buildKeyed = EFn.mkEffectFn6 \(VDomSpec spec) build ns1 name1 as1 keyedChildren1 patchKeyed ∷ ∀ a w. EFn.EffectFn2 (KeyedState a w) (VDom a w) (VDomStep a w) patchKeyed = EFn.mkEffectFn2 \state vdom → do - EFn.runEffectFn2 warnAny "patchKeyed" { state, vdom } let { build, node, attrs, ns: ns1, name: name1, children: keyedChildren1, length: len1 } = state case vdom of Grafted g → @@ -169,7 +163,6 @@ patchKeyed = EFn.mkEffectFn2 \state vdom → do haltKeyed ∷ ∀ a w. EFn.EffectFn1 (KeyedState a w) Unit haltKeyed = EFn.mkEffectFn1 \{ node, attrs, children } → do - EFn.runEffectFn2 warnAny "haltKeyed" { node, attrs, children } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent EFn.runEffectFn2 Util.forInE children (EFn.mkEffectFn2 \_ s → EFn.runEffectFn1 halt s) diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index eed5f35..fadf5f7 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -20,24 +20,24 @@ import Web.DOM.Element (Element) as DOM checkAttributeExistsAndIsEqual ∷ Maybe Namespace → String → String → DOM.Element → Effect Unit checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue element = do - elementValue ← (EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element) <#> toMaybe + elementValue ← EFn.runEffectFn3 Util.getAttribute (toNullable maybeNamespace) attributeName element <#> toMaybe + case elementValue of Nothing → do - EFn.runEffectFn2 Util.warnAny "checkAttributeExistsAndIsEqual -> missing" { element } - throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it is missing" - Just elementValue' → do - EFn.runEffectFn2 Util.warnAny "checkAttributeExistsAndIsEqual -> not missing" { elementValue', expectedElementValue, meta: { maybeNamespace, attributeName, expectedElementValue, element } } - unless (elementValue' == expectedElementValue) (do - throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it was equal to " <> Util.quote elementValue' - ) + EFn.runEffectFn2 Util.warnAny "Error info: " { element } + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it is missing (check warning above for more information)" + Just elementValue' → + unless (elementValue' == expectedElementValue) do + EFn.runEffectFn2 Util.warnAny "Error info: " { element } + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it was equal to " <> Util.quote elementValue' <> " (check warning above for more information)" checkPropExistsAndIsEqual ∷ String → PropValue → DOM.Element → Effect Unit checkPropExistsAndIsEqual propName expectedPropValue element = do let propValue = Fn.runFn2 unsafeGetProperty propName element - EFn.runEffectFn2 Util.warnAny "checkPropExistsAndIsEqual" { propValue, expectedPropValue, meta: { element, propName } } - unless (Fn.runFn2 Util.refEq propValue expectedPropValue) (do - throwException $ error $ "Expected element to have a prop " <> Util.quote propName <> " eq to " <> Util.quote (Util.anyToString expectedPropValue) <> ", but it was equal to " <> Util.quote (Util.anyToString propValue) - ) + + unless (Fn.runFn2 Util.refEq propValue expectedPropValue) do + EFn.runEffectFn2 Util.warnAny "Error info: " { element, propValue, expectedPropValue } + throwException $ error $ "Expected element to have a prop " <> Util.quote propName <> " eq to " <> Util.quote (Util.anyToString expectedPropValue) <> ", but it was equal to " <> Util.quote (Util.anyToString propValue) <> " (check warning above for more information)" -- | Inspired by https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030 mkExtraAttributeNames ∷ DOM.Element → Effect (Set.Set String) @@ -49,8 +49,7 @@ mkExtraAttributeNames el = do pure set checkExtraAttributeNamesIsEmpty ∷ Set.Set String -> DOM.Element -> Effect Unit -checkExtraAttributeNamesIsEmpty extraAttributeNames element = do - EFn.runEffectFn2 Util.warnAny "checkExtraAttributeNamesIsEmpty" { extraAttributeNames, meta: { element } } - when (Set.size extraAttributeNames > 0) (do - throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") - ) +checkExtraAttributeNamesIsEmpty extraAttributeNames element = + when (Set.size extraAttributeNames > 0) do + EFn.runEffectFn2 Util.warnAny "Error info: " { extraAttributeNames, element } + throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") <> " (check warning above for more information)" diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 07d2cb1..16e4f25 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -5,7 +5,6 @@ import Prelude import Data.Function.Uncurried as Fn import Data.Maybe (Maybe(..)) import Data.Nullable (toNullable) -import Data.String (joinWith) import Data.String.Common (toLower) import Data.Tuple (Tuple(..), fst, snd) import Effect (Effect) @@ -27,11 +26,13 @@ import Foreign (unsafeToForeign, typeOf) import Unsafe.Coerce (unsafeCoerce) deleteRequiredElement :: EFn.EffectFn2 String (Set.Set String) Unit -deleteRequiredElement = EFn.mkEffectFn2 \element set -> do - let isPresent = Fn.runFn2 Set.has element set +deleteRequiredElement = EFn.mkEffectFn2 \element extraAttributeNames -> do + let isPresent = Fn.runFn2 Set.has element extraAttributeNames if isPresent - then EFn.runEffectFn2 Set.delete element set - else throwException $ error $ "Cannot delete element that is not present in set, " <> quote element <> " should be present in set" <> (Set.toArray set # joinWith ", ") + then EFn.runEffectFn2 Set.delete element extraAttributeNames + else do + EFn.runEffectFn2 Util.warnAny "Error info: " { element, extraAttributeNames } + throwException $ error $ "Cannot delete element " <> quote element <> " that is not present in extraAttributeNames (check warning above for more information)" hydrateApplyProp ∷ ∀ a @@ -49,27 +50,27 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames pure v Property propName val → do - -- | EFn.runEffectFn2 warnAny "checkPropExistsAndIsEqual" { propName, val, el, extraAttributeNames } - case propName of "className" -> do checkPropExistsAndIsEqual propName val el EFn.runEffectFn2 deleteRequiredElement "class" extraAttributeNames "href" -> do - -- | becuase on
- -- | $0.href is eq to "http://localhost:3000/foo" - -- | but - -- | $0.attributes.href.value is eq to "/foo" - -- | $0.getAttribute("href") is eq to "/foo" + -- | We use custom check (i.e. checking attribute instead of property) because: + -- | with + -- | property $0.href is eq to "http://localhost:3000/foo" + -- | but attribute + -- | $0.attributes.href.value is eq to "/foo" + -- | $0.getAttribute("href") is eq to "/foo" too + -- | + -- | The same is true for elements also - -- | TODO: check it's on the element checkAttributeExistsAndIsEqual Nothing "href" (anyToString val) el EFn.runEffectFn2 deleteRequiredElement "href" extraAttributeNames _ -> do checkPropExistsAndIsEqual propName val el let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` case typeOf (unsafeToForeign val), (unsafeCoerce :: PropValue -> Boolean) val of - -- | if this is a boolean and is false - then it should not have been prerendered + -- | If this is a boolean and is false - then it should not have been prerendered -- | -- | For example: -- | `HH.button [HP.disabled false] []` should be rendered as `` @@ -84,8 +85,6 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect "boolean", false -> pure unit _, _ -> EFn.runEffectFn2 deleteRequiredElement 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 diff --git a/src/Halogen/VDom/DOM/Text.purs b/src/Halogen/VDom/DOM/Text.purs index a11ddf1..f19cec5 100644 --- a/src/Halogen/VDom/DOM/Text.purs +++ b/src/Halogen/VDom/DOM/Text.purs @@ -5,7 +5,6 @@ import Halogen.VDom.DOM.Checkers (checkIsTextNode, checkTextContentIsEqTo) import Halogen.VDom.DOM.Types (VDomBuilder, VDomHydrator, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep) import Halogen.VDom.Machine (Step'(..), mkStep) import Halogen.VDom.Types (VDom(..), runGraft) -import Halogen.VDom.Util (warnAny) import Halogen.VDom.Util as Util import Prelude (Unit, bind, discard, otherwise, pure, ($), (==)) import Web.DOM.Node (Node) as DOM @@ -16,10 +15,8 @@ type TextState a w = , value ∷ String } --- TODO: rename this to `hydrateTextDebug` and add another function `hydrateText` but without checks? hydrateText ∷ ∀ a w. VDomHydrator String a w hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpecWithHydration spec) _hydrate build s → do - EFn.runEffectFn2 warnAny "hydrateText" { s } currentText <- checkIsTextNode currentNode checkTextContentIsEqTo s currentText let (state :: TextState a w) = { build, node: currentNode, value: s } @@ -27,14 +24,12 @@ hydrateText = EFn.mkEffectFn5 \currentNode (VDomSpecWithHydration spec) _hydrate buildText ∷ ∀ a w. VDomBuilder String a w buildText = EFn.mkEffectFn3 \(VDomSpec spec) build s → do - EFn.runEffectFn2 warnAny "buildText" { s } node ← EFn.runEffectFn2 Util.createTextNode s spec.document let (state :: TextState a w) = { build, node, value: s } pure $ mkStep $ Step node state patchText haltText patchText ∷ ∀ a w. EFn.EffectFn2 (TextState a w) (VDom a w) (VDomStep a w) patchText = EFn.mkEffectFn2 \state vdom → do - EFn.runEffectFn2 warnAny "patchText" { state, vdom } let { build, node, value: value1 } = state case vdom of Grafted g → @@ -52,6 +47,5 @@ patchText = EFn.mkEffectFn2 \state vdom → do haltText ∷ ∀ a w. EFn.EffectFn1 (TextState a w) Unit haltText = EFn.mkEffectFn1 \{ node } → do - EFn.runEffectFn2 warnAny "haltText" { node } parent ← EFn.runEffectFn1 Util.parentNode node EFn.runEffectFn2 Util.removeChild node parent diff --git a/src/Halogen/VDom/DOM/Util.purs b/src/Halogen/VDom/DOM/Util.purs index b63fcbf..c462ed6 100644 --- a/src/Halogen/VDom/DOM/Util.purs +++ b/src/Halogen/VDom/DOM/Util.purs @@ -2,7 +2,6 @@ module Halogen.VDom.DOM.Util where import Prelude -import Data.Array (fromFoldable) as Array import Halogen.VDom.Types (VDom(..)) import Data.List as List import Data.List (List(..), (:)) @@ -10,18 +9,14 @@ import Web.DOM as DOM import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM.Node import Web.DOM.Text as DOM.Text -import Web.DOM.NodeList as DOM.NodeList import Web.DOM.Document as DOM.Document import Web.DOM.CharacterData as DOM.CharacterData -import Data.Maybe (Maybe(..)) +import Data.Maybe (Maybe) import Control.Alt ((<|>)) -import Halogen.VDom.DOM.Types (VDomBuilder4, VDomHydrator4, VDomMachine, VDomSpec(..), VDomStep) -import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn import Halogen.VDom.Util as Util import Data.String as String -import Unsafe.Coerce (unsafeCoerce) data ElementOrTextNode = ElementNode DOM.Element | TextNode DOM.Text @@ -61,14 +56,14 @@ zipChildrenAndSplitTextNodes (List ElementOrTextNode) (List vdomContainer) (List output) -zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom document parent domChildren vdomChildren -> - case domChildren, vdomChildren of - _, (vdomChild : vdomChildrenTail) -> - let vdomChild' = extractVdom vdomChild - in case domChildren, vdomChild' of +zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom document parent domChildren vdomContainerChildren -> + case domChildren, vdomContainerChildren of + Nil, Nil -> pure Nil + _, (vdomContainerChild : vdomContainerChildrenTail) -> + let vdomChild = extractVdom vdomContainerChild + in case domChildren, vdomChild of + -- | Expected text is "", but it wasnt rendered in dom (it is never rendered) - add the "" to the dom _, Text "" -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 1" { parent, domChildren, vdomChildrenTail } - (newChildWithEmptyText :: DOM.Text) <- DOM.Document.createTextNode "" document case domChildren of @@ -79,13 +74,16 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom document pa let (referenceNode' :: DOM.Node) = elementOrTextNodeToNode referenceNode void $ DOM.Node.insertBefore (DOM.Text.toNode newChildWithEmptyText) referenceNode' parent - let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomChild + let (head :: output) = toOutput (TextNode newChildWithEmptyText) vdomContainerChild - (tailResult :: List output) <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildren vdomChildrenTail + (tailResult :: List output) <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildren vdomContainerChildrenTail pure (head : tailResult) + -- | We have Text in dom and Text in vdom + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobarbaz"]` - throw error (LT case) + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate the node (EQ case) + -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" (GT case) (TextNode textNode : domChildrenTail), (Text expectedText) -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2" { parent, textNode, domChildrenTail, expectedText, vdomChildrenTail } textNodeLength <- DOM.CharacterData.length (DOM.Text.toCharacterData textNode) let expectedTextLength = String.length expectedText @@ -93,37 +91,33 @@ zipChildrenAndSplitTextNodes = EFn.mkEffectFn6 \toOutput extractVdom document pa case compare textNodeLength expectedTextLength of LT -> do textNodeData <- DOM.CharacterData.data_ (DOM.Text.toCharacterData textNode) - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 LT" { } - throwException $ error $ "should not smaller then expected " <> textNodeData -- TODO: better errors - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foobar"]` - it should just hydrate - EQ -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 EQ" { } + EFn.runEffectFn2 Util.warnAny "Error info: " { textNode, expectedText } - let (head :: output) = toOutput (TextNode textNode) vdomChild + throwException $ error $ "[zipChildrenAndSplitTextNodes] The Text node length should not smaller then expected. Expected length = " <> show expectedTextLength <> ", actual = " <> show textNodeLength <> " (check warning above for more information)" + EQ -> do + let (head :: output) = toOutput (TextNode textNode) vdomContainerChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomContainerChildrenTail pure (head : tailResult) - - -- | when DOM is `
foobar
` and vdom is `HH.div_ [HH.text "foo", HH.text "bar"]` - it should split "foobar" on "foo" and "bar" GT -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 2 GT" { } nextTextNode <- DOM.Text.splitText expectedTextLength textNode -- this is our "bar", and textNode is now our "foo" (but was - "foobar") - let (head :: output) = toOutput (TextNode textNode) vdomChild + let (head :: output) = toOutput (TextNode textNode) vdomContainerChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent (TextNode nextTextNode : domChildrenTail) vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent (TextNode nextTextNode : domChildrenTail) vdomContainerChildrenTail pure (head : tailResult) (domChild : domChildrenTail), _ -> do - EFn.runEffectFn2 Util.warnAny "zipChildrenAndSplitTextNodes 3" {} - - let (head :: output) = toOutput domChild vdomChild + let (head :: output) = toOutput domChild vdomContainerChild - tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomChildrenTail + tailResult <- EFn.runEffectFn6 zipChildrenAndSplitTextNodes toOutput extractVdom document parent domChildrenTail vdomContainerChildrenTail pure (head : tailResult) - _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" - Nil, Nil -> pure Nil - _, _ -> throwException $ error $ "[zipChildrenAndSplitTextNodes] unexpected input" + domChildren', vdomChild' -> do + EFn.runEffectFn2 Util.warnAny "Error info: " { domChildren: domChildren', vdomChild: vdomChild' } + throwException $ error $ "[zipChildrenAndSplitTextNodes] Unexpected input (check warning above for more information)" + domChildren', vdomChildren' -> do + EFn.runEffectFn2 Util.warnAny "Error info: " { domChildren: domChildren', vdomChildren: vdomChildren' } + throwException $ error $ "[zipChildrenAndSplitTextNodes] Unexpected input (check warning above for more information)" diff --git a/src/Halogen/VDom/DOM/Widget.purs b/src/Halogen/VDom/DOM/Widget.purs index d3650d7..9fc0972 100644 --- a/src/Halogen/VDom/DOM/Widget.purs +++ b/src/Halogen/VDom/DOM/Widget.purs @@ -4,7 +4,6 @@ import Effect.Uncurried as EFn import Halogen.VDom.DOM.Types (VDomBuilder, VDomMachine, VDomSpec(..), VDomSpecWithHydration(..), VDomStep, VDomHydrator) import Halogen.VDom.Machine (Step, Step'(..), halt, mkStep, step, unStep) import Halogen.VDom.Types (VDom(..), runGraft) -import Halogen.VDom.Util (warnAny) import Prelude (Unit, bind, discard, pure, (#), ($)) import Web.DOM.Node (Node) as DOM @@ -15,7 +14,6 @@ type WidgetState a w = hydrateWidget ∷ ∀ a w. VDomHydrator w a w hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpecWithHydration spec) _hydrate build w → do - EFn.runEffectFn2 warnAny "hydrateWidget" { w } res ← EFn.runEffectFn1 (spec.hydrateWidget (VDomSpecWithHydration spec) elem) w let res' :: Step (VDom a w) DOM.Node @@ -25,7 +23,6 @@ hydrateWidget = EFn.mkEffectFn5 \elem (VDomSpecWithHydration spec) _hydrate buil buildWidget ∷ ∀ a w. VDomBuilder w a w buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do - EFn.runEffectFn2 warnAny "buildWidget" { w } res ← EFn.runEffectFn1 (spec.buildWidget (VDomSpec spec)) w let res' :: Step (VDom a w) DOM.Node @@ -35,7 +32,6 @@ buildWidget = EFn.mkEffectFn3 \(VDomSpec spec) build w → do patchWidget ∷ ∀ a w. EFn.EffectFn2 (WidgetState a w) (VDom a w) (VDomStep a w) patchWidget = EFn.mkEffectFn2 \state vdom → do - EFn.runEffectFn2 warnAny "patchWidget" { state, vdom } let { build, widget } = state case vdom of Grafted g → @@ -52,5 +48,4 @@ patchWidget = EFn.mkEffectFn2 \state vdom → do haltWidget ∷ forall a w. EFn.EffectFn1 (WidgetState a w) Unit haltWidget = EFn.mkEffectFn1 \{ widget } → do - EFn.runEffectFn2 warnAny "haltWidget" { widget } EFn.runEffectFn1 halt widget diff --git a/src/Halogen/VDom/Types.purs b/src/Halogen/VDom/Types.purs index 9692e22..b6a704a 100644 --- a/src/Halogen/VDom/Types.purs +++ b/src/Halogen/VDom/Types.purs @@ -25,7 +25,7 @@ import Unsafe.Coerce (unsafeCoerce) -- | fusion using a Coyoneda-like encoding. data VDom a w = Text String - | Elem (Maybe Namespace) ElemName a (Array (VDom a w)) -- TODO: use list instead of array, as elm doint it https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.js#L1531 + | Elem (Maybe Namespace) ElemName a (Array (VDom a w)) | Keyed (Maybe Namespace) ElemName a (Array (Tuple String (VDom a w))) | Widget w | Grafted (Graft a w) diff --git a/test/Hydration.purs b/test/Hydration.purs index 7ad444e..2819457 100644 --- a/test/Hydration.purs +++ b/test/Hydration.purs @@ -76,61 +76,64 @@ main = do EFn.runEffectFn3 Util.addEventListener "click" listener updateStateButton -tests ∷ Array { client ∷ String , errorMessage ∷ String , server ∷ String , title ∷ String } -tests = - -- | [ { title: "Attribute → renders" - [ { title: "Attribute → missing prop" - , server: """ -
test label 1
- """ - , client: """ -
test label 1
- """ - , errorMessage: """ - Warning: Prop `%s` did not match. Server: %s Client: %s%s - """ - } - , { title: "Attribute → extra prop" - , server: """ -
test label 1
- """ - , client: """ -
test label 1
- """ - , errorMessage: """ - Warning: Extra attributes from the server: %s%s - """ - } - , { title: "Attribute → did not match" - , server: """ -
test label 1
- """ - , client: """ -
test label 1
- """ - , errorMessage: """ - Warning: Prop `%s` did not match. Server: %s Client: %s%s - """ - } - -- | , { title: "Prop → boolean → " - , { title: "Prop → controlled element → renders" - , server: """ - - """ - , client: """ - - """ - , errorMessage: """ - """ - } - , { title: "Prop → controlled element → did not match → renders" - , server: """ - - """ - , client: """ - - """ - , errorMessage: """ - """ - } - ] +-- | TODO +-- | +-- | tests ∷ Array { client ∷ String , errorMessage ∷ String , server ∷ String , title ∷ String } +-- | tests = +-- | -- | [ { title: "Attribute → renders" +-- | [ { title: "Attribute → missing prop" +-- | , server: """ +-- |
test label 1
+-- | """ +-- | , client: """ +-- |
test label 1
+-- | """ +-- | , errorMessage: """ +-- | Warning: Prop `%s` did not match. Server: %s Client: %s%s +-- | """ +-- | } +-- | , { title: "Attribute → extra prop" +-- | , server: """ +-- |
test label 1
+-- | """ +-- | , client: """ +-- |
test label 1
+-- | """ +-- | , errorMessage: """ +-- | Warning: Extra attributes from the server: %s%s +-- | """ +-- | } +-- | , { title: "Attribute → did not match" +-- | , server: """ +-- |
test label 1
+-- | """ +-- | , client: """ +-- |
test label 1
+-- | """ +-- | , errorMessage: """ +-- | Warning: Prop `%s` did not match. Server: %s Client: %s%s +-- | """ +-- | } +-- | -- | , { title: "Prop → boolean → " +-- | , { title: "Prop → controlled element → renders" +-- | , server: """ +-- | +-- | """ +-- | , client: """ +-- | +-- | """ +-- | , errorMessage: """ +-- | """ +-- | } +-- | , { title: "Prop → controlled element → did not match → renders" +-- | , server: """ +-- | +-- | """ +-- | , client: """ +-- | +-- | """ +-- | , errorMessage: """ +-- | """ +-- | } +-- | ] +-- | From c0217b8d6afceadc48f656f0b121a1765ad44070 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 24 Jun 2020 21:40:26 +0300 Subject: [PATCH 39/48] refactor: remove debug package --- spago.dhall | 1 - 1 file changed, 1 deletion(-) diff --git a/spago.dhall b/spago.dhall index 05a96f8..9cd6c00 100644 --- a/spago.dhall +++ b/spago.dhall @@ -19,7 +19,6 @@ You can edit this file as you like. , "unsafe-coerce" , "web-html" , "web-dom" - , "debug" , "strings" , "control" , "lazy" From 50c315a1fdb2a547830a8ed49c9ea93fec1e6025 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Fri, 24 Jul 2020 11:58:06 +0300 Subject: [PATCH 40/48] feat(#41): hydration -> additional props with name different from attribute -> handle --- src/Halogen/VDom/DOM/Prop/Implementation.purs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 16e4f25..25ab20e 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -34,6 +34,11 @@ deleteRequiredElement = EFn.mkEffectFn2 \element extraAttributeNames -> do EFn.runEffectFn2 Util.warnAny "Error info: " { element, extraAttributeNames } throwException $ error $ "Cannot delete element " <> quote element <> " that is not present in extraAttributeNames (check warning above for more information)" +checkPropExistsAndIsEqualAndDelete :: EFn.EffectFn5 (Set.Set String) String PropValue DOM.Element String Unit +checkPropExistsAndIsEqualAndDelete = EFn.mkEffectFn5 \extraAttributeNames propName val el correspondingAttributeName -> do + checkPropExistsAndIsEqual propName val el + EFn.runEffectFn2 deleteRequiredElement correspondingAttributeName extraAttributeNames + hydrateApplyProp ∷ ∀ a . Fn.Fn4 @@ -51,24 +56,24 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect pure v Property propName val → do case propName of - "className" -> do - checkPropExistsAndIsEqual propName val el - EFn.runEffectFn2 deleteRequiredElement "class" extraAttributeNames + -- | We use custom check for "href" (i.e. checking attribute instead of property) because: + -- | with
+ -- | property $0.href is eq to "http://localhost:3000/foo" + -- | but attribute + -- | $0.attributes.href.value is eq to "/foo" + -- | $0.getAttribute("href") is eq to "/foo" too + -- | + -- | The same is true for elements also "href" -> do - -- | We use custom check (i.e. checking attribute instead of property) because: - -- | with - -- | property $0.href is eq to "http://localhost:3000/foo" - -- | but attribute - -- | $0.attributes.href.value is eq to "/foo" - -- | $0.getAttribute("href") is eq to "/foo" too - -- | - -- | The same is true for elements also - checkAttributeExistsAndIsEqual Nothing "href" (anyToString val) el EFn.runEffectFn2 deleteRequiredElement "href" extraAttributeNames + -- | these 4 property names are taken from https://github.com/elm/virtual-dom/blob/5a5bcf48720bc7d53461b3cd42a9f19f119c5503/src/Elm/Kernel/VirtualDom.server.js#L196-L201 + "className" -> EFn.runEffectFn5 checkPropExistsAndIsEqualAndDelete extraAttributeNames propName val el "class" + "htmlFor" -> EFn.runEffectFn5 checkPropExistsAndIsEqualAndDelete extraAttributeNames propName val el "for" + "httpEquiv" -> EFn.runEffectFn5 checkPropExistsAndIsEqualAndDelete extraAttributeNames propName val el "http-equiv" + "acceptCharset" -> EFn.runEffectFn5 checkPropExistsAndIsEqualAndDelete extraAttributeNames propName val el "accept-charset" _ -> do checkPropExistsAndIsEqual propName val el - let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` case typeOf (unsafeToForeign val), (unsafeCoerce :: PropValue -> Boolean) val of -- | If this is a boolean and is false - then it should not have been prerendered -- | @@ -83,7 +88,9 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect -- | `` the `$0.disabled === true` -- | `` the `$0.disabled === false` "boolean", false -> pure unit - _, _ -> EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames + _, _ -> + let fullAttributeName' = toLower propName -- transforms `colSpan` to `colspan` + in EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames pure v Handler eventType emitterInputBuilder → do From 312ef166105b1b4eedad7b45002ec666d81addf3 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Fri, 31 Jul 2020 21:58:05 +0300 Subject: [PATCH 41/48] fix: error with element with namespace "Expected element tagName equal to "HTTP://WWW.W3.ORG/2000/FOOOOO:SVGFOOP", but got "SVGFOOP" (check warning above for more information)" --- src/Halogen/VDom/DOM/Checkers.purs | 30 ++++++++++++------- src/Halogen/VDom/DOM/Prop/Checkers.purs | 6 ++-- src/Halogen/VDom/DOM/Prop/Implementation.purs | 3 +- src/Halogen/VDom/Util.purs | 8 ++--- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index bd7aa28..8b8c71a 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -3,19 +3,19 @@ module Halogen.VDom.DOM.Checkers where import Prelude import Data.Maybe (Maybe(..)) -import Data.String (toUpper) import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Halogen.VDom.Types (ElemName, Namespace) +import Halogen.VDom.Types (ElemName(..), Namespace) import Halogen.VDom.Util as Util import Partial.Unsafe (unsafePartial) +import Unsafe.Coerce (unsafeCoerce) import Web.DOM as DOM +import Web.DOM.CharacterData as DOM.CharacterData import Web.DOM.Element as DOM.Element import Web.DOM.Node as DOM.Node import Web.DOM.NodeType as DOM.NodeType import Web.DOM.Text as DOM.Text -import Web.DOM.CharacterData as DOM.CharacterData -- | Text checkers @@ -45,12 +45,22 @@ checkIsElementNode node = throwException $ error $ "Expected node to be a " <> show DOM.NodeType.ElementNode <> ", but got " <> show (unsafePartial (DOM.Node.nodeType node)) <> " (check warning above for more information)" checkTagNameIsEqualTo :: Maybe Namespace -> ElemName -> DOM.Element -> Effect Unit -checkTagNameIsEqualTo maybeNamespace elemName element = do +checkTagNameIsEqualTo maybeNamespace (ElemName elemName) element = do + let + localName :: String + localName = DOM.Element.localName element + + when (localName /= elemName) do + EFn.runEffectFn2 Util.warnAny "Error info: " { maybeNamespace, elemName, element } + throwException $ error $ "Expected element localName equal to " <> show elemName <> ", but got " <> show localName <> " (check warning above for more information)" + let - -- e.g. `DIV` or `FOO:SVG` - expectedTagName :: String - expectedTagName = toUpper $ Util.fullAttributeName maybeNamespace elemName - let tagName = DOM.Element.tagName element - when (tagName /= expectedTagName) do + maybeNamespace' :: Maybe String + maybeNamespace' = unsafeCoerce maybeNamespace + + namespaceURI :: Maybe String + namespaceURI = DOM.Element.namespaceURI element + + when (namespaceURI /= maybeNamespace') do EFn.runEffectFn2 Util.warnAny "Error info: " { maybeNamespace, elemName, element } - throwException $ error $ "Expected element tagName equal to " <> show expectedTagName <> ", but got " <> show tagName <> " (check warning above for more information)" + throwException $ error $ "Expected element namespaceURI equal to " <> show maybeNamespace' <> ", but got " <> show namespaceURI <> " (check warning above for more information)" diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index fadf5f7..996b6ef 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -14,7 +14,7 @@ import Halogen.VDom.Attributes (attributes, forEachE) as Attributes import Halogen.VDom.DOM.Prop.Types (PropValue) import Halogen.VDom.DOM.Prop.Util (unsafeGetProperty) import Halogen.VDom.Set as Set -import Halogen.VDom.Types (ElemName(..), Namespace) +import Halogen.VDom.Types (Namespace) import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM @@ -25,11 +25,11 @@ checkAttributeExistsAndIsEqual maybeNamespace attributeName expectedElementValue case elementValue of Nothing → do EFn.runEffectFn2 Util.warnAny "Error info: " { element } - throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it is missing (check warning above for more information)" + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace attributeName) <> " eq to " <> Util.quote expectedElementValue <> ", but it is missing (check warning above for more information)" Just elementValue' → unless (elementValue' == expectedElementValue) do EFn.runEffectFn2 Util.warnAny "Error info: " { element } - throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace (ElemName attributeName)) <> " eq to " <> Util.quote expectedElementValue <> ", but it was equal to " <> Util.quote elementValue' <> " (check warning above for more information)" + throwException $ error $ "Expected element to have an attribute " <> Util.quote (Util.fullAttributeName maybeNamespace attributeName) <> " eq to " <> Util.quote expectedElementValue <> ", but it was equal to " <> Util.quote elementValue' <> " (check warning above for more information)" checkPropExistsAndIsEqual ∷ String → PropValue → DOM.Element → Effect Unit checkPropExistsAndIsEqual propName expectedPropValue element = do diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 25ab20e..5a8f471 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -16,7 +16,6 @@ import Halogen.VDom.DOM.Prop.Checkers (checkAttributeExistsAndIsEqual, checkProp import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropValue) import Halogen.VDom.DOM.Prop.Util (removeProperty, setProperty, unsafeGetProperty) import Halogen.VDom.Set as Set -import Halogen.VDom.Types (ElemName(..)) import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM @@ -51,7 +50,7 @@ hydrateApplyProp = Fn.mkFn4 \extraAttributeNames el emit events → EFn.mkEffect case v of Attribute maybeNamespace attributeName val → do checkAttributeExistsAndIsEqual maybeNamespace attributeName val el - let fullAttributeName' = fullAttributeName maybeNamespace (ElemName attributeName) -- should be lowercased + let fullAttributeName' = fullAttributeName maybeNamespace attributeName -- should be lowercased EFn.runEffectFn2 deleteRequiredElement fullAttributeName' extraAttributeNames pure v Property propName val → do diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index 6063914..fc61124 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -149,11 +149,11 @@ foreign import warnAny ∷ ∀ a . EFn.EffectFn2 String a Unit foreign import logAny ∷ ∀ a . EFn.EffectFn2 String a Unit -fullAttributeName ∷ Maybe Namespace → ElemName → String -fullAttributeName maybeNamespace (ElemName elemName) = +fullAttributeName ∷ Maybe Namespace → String → String +fullAttributeName maybeNamespace attributeName = case maybeNamespace of - Just (Namespace namespace) -> namespace <> ":" <> elemName - Nothing -> elemName + Just (Namespace namespace) -> namespace <> ":" <> attributeName + Nothing -> attributeName eqElemSpec ∷ Fn.Fn4 (Maybe Namespace) ElemName (Maybe Namespace) ElemName Boolean eqElemSpec = Fn.mkFn4 \ns1 (ElemName name1) ns2 (ElemName name2) → From f98a5dace6b9ee9ddf7a6b0f2007ed62a037ccdc Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 1 Aug 2020 11:16:27 +0300 Subject: [PATCH 42/48] feat: namespaces -> dont check default namespace --- src/Halogen/VDom/DOM/Checkers.purs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 8b8c71a..5245af1 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -6,7 +6,7 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Halogen.VDom.Types (ElemName(..), Namespace) +import Halogen.VDom.Types (ElemName(..), Namespace(..)) import Halogen.VDom.Util as Util import Partial.Unsafe (unsafePartial) import Unsafe.Coerce (unsafeCoerce) @@ -61,6 +61,13 @@ checkTagNameIsEqualTo maybeNamespace (ElemName elemName) element = do namespaceURI :: Maybe String namespaceURI = DOM.Element.namespaceURI element - when (namespaceURI /= maybeNamespace') do + -- | default namespace is "http://www.w3.org/1999/xhtml" + namespaceURI' :: Maybe String + namespaceURI' = + case namespaceURI of + Just "http://www.w3.org/1999/xhtml" -> Nothing + other -> other + + when (namespaceURI' /= maybeNamespace') do EFn.runEffectFn2 Util.warnAny "Error info: " { maybeNamespace, elemName, element } - throwException $ error $ "Expected element namespaceURI equal to " <> show maybeNamespace' <> ", but got " <> show namespaceURI <> " (check warning above for more information)" + throwException $ error $ "Expected element namespaceURI equal to " <> show maybeNamespace' <> ", but got " <> show namespaceURI' <> " (check warning above for more information)" From 1cc8aef67efbe2f4cfaed8aae5a3eab58f8527a5 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Sat, 1 Aug 2020 11:28:07 +0300 Subject: [PATCH 43/48] fix: Cannot delete element "viewBox" that is not present in extraAttributeNames (check warning above for more information) Error info: element: "viewBox" extraAttributeNames: Set(1) {"viewbox"} Reason: I misunderstood https://github.com/facebook/react/blob/823dc581fea8814a904579e85a62da6d18258830/packages/react-dom/src/client/ReactDOMComponent.js#L1030 the names are not really lowercased when they are added to a Set --- src/Halogen/VDom/DOM/Prop/Checkers.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 996b6ef..133bc17 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -45,7 +45,7 @@ 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) + EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \attribute → EFn.runEffectFn2 Set.add attribute.name set) pure set checkExtraAttributeNamesIsEmpty ∷ Set.Set String -> DOM.Element -> Effect Unit From 40b403468c8e0c70ecf9f807e3dd87a623e1cb50 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Mon, 7 Sep 2020 20:18:08 +0300 Subject: [PATCH 44/48] fix: warnings --- src/Halogen/VDom/DOM/Checkers.purs | 2 +- src/Halogen/VDom/DOM/Prop/Checkers.purs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Halogen/VDom/DOM/Checkers.purs b/src/Halogen/VDom/DOM/Checkers.purs index 5245af1..b759435 100644 --- a/src/Halogen/VDom/DOM/Checkers.purs +++ b/src/Halogen/VDom/DOM/Checkers.purs @@ -6,7 +6,7 @@ import Data.Maybe (Maybe(..)) import Effect (Effect) import Effect.Exception (error, throwException) import Effect.Uncurried as EFn -import Halogen.VDom.Types (ElemName(..), Namespace(..)) +import Halogen.VDom.Types (ElemName(..), Namespace) import Halogen.VDom.Util as Util import Partial.Unsafe (unsafePartial) import Unsafe.Coerce (unsafeCoerce) diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 133bc17..e00bde8 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -5,7 +5,6 @@ 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) From bd7744e243ec37ad03702edd31a660f58bdd0a20 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Tue, 8 Sep 2020 17:25:51 +0300 Subject: [PATCH 45/48] fix: better errors -> show propsToHydrate --- src/Halogen/VDom/DOM/Prop.purs | 2 +- src/Halogen/VDom/DOM/Prop/Checkers.purs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Halogen/VDom/DOM/Prop.purs b/src/Halogen/VDom/DOM/Prop.purs index 1cfc5d9..d64a0b5 100644 --- a/src/Halogen/VDom/DOM/Prop.purs +++ b/src/Halogen/VDom/DOM/Prop.purs @@ -33,7 +33,7 @@ hydrateProp emit el = renderProp (props ∷ Object.Object (Prop a)) ← EFn.runEffectFn3 Util.strMapWithIxE ps1 propToStrKey (Fn.runFn4 hydrateApplyProp extraAttributeNames el emit events) - checkExtraAttributeNamesIsEmpty extraAttributeNames el + checkExtraAttributeNamesIsEmpty ps1 extraAttributeNames el let (state ∷ PropState a) = diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index e00bde8..7ab1815 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -10,7 +10,7 @@ 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.Types (Prop, PropValue) import Halogen.VDom.DOM.Prop.Util (unsafeGetProperty) import Halogen.VDom.Set as Set import Halogen.VDom.Types (Namespace) @@ -47,8 +47,8 @@ mkExtraAttributeNames el = do EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \attribute → EFn.runEffectFn2 Set.add attribute.name set) pure set -checkExtraAttributeNamesIsEmpty ∷ Set.Set String -> DOM.Element -> Effect Unit -checkExtraAttributeNamesIsEmpty extraAttributeNames element = +checkExtraAttributeNamesIsEmpty ∷ forall a . Array (Prop a) -> Set.Set String -> DOM.Element -> Effect Unit +checkExtraAttributeNamesIsEmpty propsToHydrate extraAttributeNames element = when (Set.size extraAttributeNames > 0) do - EFn.runEffectFn2 Util.warnAny "Error info: " { extraAttributeNames, element } + EFn.runEffectFn2 Util.warnAny "Error info: " { extraAttributeNames, element, propsToHydrate } throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") <> " (check warning above for more information)" From 2f5dc5251679127c0fafa1db57bb3020eb593ea3 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 7 Oct 2020 12:39:12 +0300 Subject: [PATCH 46/48] feat: rename set to jsset --- src/Halogen/VDom/DOM/Prop/Checkers.purs | 19 ++++++++------- src/Halogen/VDom/DOM/Prop/Implementation.purs | 13 ++++++----- src/Halogen/VDom/JsSet.js | 23 +++++++++++++++++++ src/Halogen/VDom/JsSet.purs | 20 ++++++++++++++++ src/Halogen/VDom/Set.js | 23 ------------------- src/Halogen/VDom/Set.purs | 21 ----------------- 6 files changed, 61 insertions(+), 58 deletions(-) create mode 100644 src/Halogen/VDom/JsSet.js create mode 100644 src/Halogen/VDom/JsSet.purs delete mode 100644 src/Halogen/VDom/Set.js delete mode 100644 src/Halogen/VDom/Set.purs diff --git a/src/Halogen/VDom/DOM/Prop/Checkers.purs b/src/Halogen/VDom/DOM/Prop/Checkers.purs index 7ab1815..9c6f9d2 100644 --- a/src/Halogen/VDom/DOM/Prop/Checkers.purs +++ b/src/Halogen/VDom/DOM/Prop/Checkers.purs @@ -12,7 +12,8 @@ import Effect.Uncurried as EFn import Halogen.VDom.Attributes (attributes, forEachE) as Attributes import Halogen.VDom.DOM.Prop.Types (Prop, PropValue) import Halogen.VDom.DOM.Prop.Util (unsafeGetProperty) -import Halogen.VDom.Set as Set +import Halogen.VDom.JsSet (JsSet) +import Halogen.VDom.JsSet as JsSet import Halogen.VDom.Types (Namespace) import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM @@ -39,16 +40,18 @@ checkPropExistsAndIsEqual propName expectedPropValue element = do throwException $ error $ "Expected element to have a prop " <> Util.quote propName <> " eq to " <> Util.quote (Util.anyToString expectedPropValue) <> ", but it was equal to " <> Util.quote (Util.anyToString propValue) <> " (check warning above for more information)" -- | 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 ∷ DOM.Element → Effect (JsSet 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 attribute.name set) + (set ∷ JsSet String) ← JsSet.empty + EFn.runEffectFn2 Attributes.forEachE namedNodeMap (EFn.mkEffectFn1 \attribute → EFn.runEffectFn2 JsSet._add attribute.name set) pure set -checkExtraAttributeNamesIsEmpty ∷ forall a . Array (Prop a) -> Set.Set String -> DOM.Element -> Effect Unit -checkExtraAttributeNamesIsEmpty propsToHydrate extraAttributeNames element = - when (Set.size extraAttributeNames > 0) do +checkExtraAttributeNamesIsEmpty ∷ forall a . Array (Prop a) -> JsSet String -> DOM.Element -> Effect Unit +checkExtraAttributeNamesIsEmpty propsToHydrate extraAttributeNames element = do + size <- EFn.runEffectFn1 JsSet._size extraAttributeNames + when (size > 0) do EFn.runEffectFn2 Util.warnAny "Error info: " { extraAttributeNames, element, propsToHydrate } - throwException $ error $ "Extra attributes from the server: " <> (Set.toArray extraAttributeNames # joinWith ", ") <> " (check warning above for more information)" + extraAttributeNames' <- EFn.runEffectFn1 JsSet._toArray extraAttributeNames + throwException $ error $ "Extra attributes from the server: " <> joinWith ", " extraAttributeNames' <> " (check warning above for more information)" diff --git a/src/Halogen/VDom/DOM/Prop/Implementation.purs b/src/Halogen/VDom/DOM/Prop/Implementation.purs index 5a8f471..d6efa28 100644 --- a/src/Halogen/VDom/DOM/Prop/Implementation.purs +++ b/src/Halogen/VDom/DOM/Prop/Implementation.purs @@ -15,7 +15,8 @@ import Foreign.Object as Object import Halogen.VDom.DOM.Prop.Checkers (checkAttributeExistsAndIsEqual, checkPropExistsAndIsEqual) import Halogen.VDom.DOM.Prop.Types (ElemRef(..), EmitterInputBuilder, EventListenerAndCurrentEmitterInputBuilder, Prop(..), PropValue) import Halogen.VDom.DOM.Prop.Util (removeProperty, setProperty, unsafeGetProperty) -import Halogen.VDom.Set as Set +import Halogen.VDom.JsSet (JsSet) +import Halogen.VDom.JsSet as JsSet import Halogen.VDom.Util (STObject', anyToString, fullAttributeName, quote) import Halogen.VDom.Util as Util import Web.DOM.Element (Element) as DOM @@ -24,16 +25,16 @@ import Web.Event.EventTarget (eventListener, EventListener) as DOM import Foreign (unsafeToForeign, typeOf) import Unsafe.Coerce (unsafeCoerce) -deleteRequiredElement :: EFn.EffectFn2 String (Set.Set String) Unit +deleteRequiredElement :: EFn.EffectFn2 String (JsSet String) Unit deleteRequiredElement = EFn.mkEffectFn2 \element extraAttributeNames -> do - let isPresent = Fn.runFn2 Set.has element extraAttributeNames + isPresent <- EFn.runEffectFn2 JsSet._has element extraAttributeNames if isPresent - then EFn.runEffectFn2 Set.delete element extraAttributeNames + then EFn.runEffectFn2 JsSet._delete element extraAttributeNames else do EFn.runEffectFn2 Util.warnAny "Error info: " { element, extraAttributeNames } throwException $ error $ "Cannot delete element " <> quote element <> " that is not present in extraAttributeNames (check warning above for more information)" -checkPropExistsAndIsEqualAndDelete :: EFn.EffectFn5 (Set.Set String) String PropValue DOM.Element String Unit +checkPropExistsAndIsEqualAndDelete :: EFn.EffectFn5 (JsSet String) String PropValue DOM.Element String Unit checkPropExistsAndIsEqualAndDelete = EFn.mkEffectFn5 \extraAttributeNames propName val el correspondingAttributeName -> do checkPropExistsAndIsEqual propName val el EFn.runEffectFn2 deleteRequiredElement correspondingAttributeName extraAttributeNames @@ -41,7 +42,7 @@ checkPropExistsAndIsEqualAndDelete = EFn.mkEffectFn5 \extraAttributeNames propNa hydrateApplyProp ∷ ∀ a . Fn.Fn4 - (Set.Set String) + (JsSet String) DOM.Element (a → Effect Unit) (STObject' (EventListenerAndCurrentEmitterInputBuilder a)) diff --git a/src/Halogen/VDom/JsSet.js b/src/Halogen/VDom/JsSet.js new file mode 100644 index 0000000..322c3bc --- /dev/null +++ b/src/Halogen/VDom/JsSet.js @@ -0,0 +1,23 @@ +exports.empty = function() { + return new Set() +} + +exports._delete = function(value, set) { + set.delete(value) +} + +exports._add = function(value, set) { + set.add(value) +} + +exports._size = function(set) { + return set.size +} + +exports._has = function(value, set) { + return set.has(value) +} + +exports._toArray = function(set) { + return Array.from(set) +} diff --git a/src/Halogen/VDom/JsSet.purs b/src/Halogen/VDom/JsSet.purs new file mode 100644 index 0000000..f0bbfde --- /dev/null +++ b/src/Halogen/VDom/JsSet.purs @@ -0,0 +1,20 @@ +module Halogen.VDom.JsSet where + +import Prelude + +import Effect (Effect) +import Effect.Uncurried (EffectFn1, EffectFn2) as EFn + +data JsSet a + +foreign import empty ∷ ∀ a . Effect (JsSet a) + +foreign import _delete ∷ ∀ a . EFn.EffectFn2 a (JsSet a) Unit + +foreign import _add ∷ ∀ a . EFn.EffectFn2 a (JsSet a) Unit + +foreign import _size ∷ ∀ a . EFn.EffectFn1 (JsSet a) Int + +foreign import _has ∷ ∀ a . EFn.EffectFn2 a (JsSet a) Boolean + +foreign import _toArray ∷ ∀ a . EFn.EffectFn1 (JsSet a) (Array a) diff --git a/src/Halogen/VDom/Set.js b/src/Halogen/VDom/Set.js deleted file mode 100644 index 61fec63..0000000 --- a/src/Halogen/VDom/Set.js +++ /dev/null @@ -1,23 +0,0 @@ -exports.empty = function() { - return new Set() -} - -exports.delete = function(value, set) { - set.delete(value) -} - -exports.add = function(value, set) { - set.add(value) -} - -exports.size = function(set) { - return set.size -} - -exports.has = function(value, set) { - return set.has(value) -} - -exports.toArray = function(set) { - return Array.from(set) -} diff --git a/src/Halogen/VDom/Set.purs b/src/Halogen/VDom/Set.purs deleted file mode 100644 index 41b5dff..0000000 --- a/src/Halogen/VDom/Set.purs +++ /dev/null @@ -1,21 +0,0 @@ -module Halogen.VDom.Set where - -import Prelude (Unit) - -import Effect (Effect) -import Effect.Uncurried (EffectFn2) as EFn -import Data.Function.Uncurried as Fn - -data Set proxy - -foreign import empty ∷ ∀ a . Effect (Set a) - -foreign import delete ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit - -foreign import add ∷ ∀ a . EFn.EffectFn2 a (Set a) Unit - -foreign import size ∷ ∀ a . Set a → Int - -foreign import has ∷ ∀ a . Fn.Fn2 a (Set a) Boolean - -foreign import toArray ∷ ∀ a . Set a → Array a From cae26b54d7cbe53f260ffb12eeda1b345f1e751d Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 3 Feb 2021 14:28:59 +0200 Subject: [PATCH 47/48] feat: update to purescript 0.14, fix warnings --- packages.dhall | 7 ++----- src/Halogen/VDom/JsSet.purs | 2 +- src/Halogen/VDom/Util.purs | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages.dhall b/packages.dhall index cb13b02..dffed93 100644 --- a/packages.dhall +++ b/packages.dhall @@ -119,12 +119,9 @@ let additions = let upstream = - https://github.com/purescript/package-sets/releases/download/psc-0.13.6-20200309/packages.dhall sha256:9221987b4e7ea99ccd0efbe056f7bebc872cd92e0058efe5baa181d73359e7b3 + /home/srghma/projects/package-sets/src/packages.dhall -let overrides = - { web-dom = - upstream.web-dom // { repo = "https://github.com/srghma/purescript-web-dom.git", version = "patch-1" } - } +let overrides = {=} let additions = {=} diff --git a/src/Halogen/VDom/JsSet.purs b/src/Halogen/VDom/JsSet.purs index f0bbfde..e95272c 100644 --- a/src/Halogen/VDom/JsSet.purs +++ b/src/Halogen/VDom/JsSet.purs @@ -5,7 +5,7 @@ import Prelude import Effect (Effect) import Effect.Uncurried (EffectFn1, EffectFn2) as EFn -data JsSet a +foreign import data JsSet :: Type -> Type foreign import empty ∷ ∀ a . Effect (JsSet a) diff --git a/src/Halogen/VDom/Util.purs b/src/Halogen/VDom/Util.purs index fc61124..08584b8 100644 --- a/src/Halogen/VDom/Util.purs +++ b/src/Halogen/VDom/Util.purs @@ -17,7 +17,7 @@ import Web.DOM.Node (Node) as DOM import Web.Event.EventTarget (EventListener) as DOM import Data.Maybe (Maybe(..)) -data STObject' a -- just like STObject, but without region +foreign import data STObject' :: Type -> Type newMutMap ∷ ∀ a. Effect (STObject' a) newMutMap = unsafeCoerce STObject.new From b2486e17bd2578611dcdb2dead6a072d82797ac9 Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Thu, 11 Mar 2021 15:06:35 +0200 Subject: [PATCH 48/48] feat: update comment for findElementFirstChild --- src/Halogen/VDom/Finders.purs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Halogen/VDom/Finders.purs b/src/Halogen/VDom/Finders.purs index 5e9176e..2254fdc 100644 --- a/src/Halogen/VDom/Finders.purs +++ b/src/Halogen/VDom/Finders.purs @@ -8,7 +8,7 @@ import Web.DOM (Element) import Web.DOM.Element (toParentNode) as DOM.Element import Web.DOM.ParentNode (firstElementChild, childElementCount) as DOM.ParentNode --- | Used for hydration +-- | Used for hydration in halogen code and in halogen-vdom tests findElementFirstChild :: Element -> Effect (Either String Element) findElementFirstChild container = do let