From f8b998f969fd0e3b3555c69e20542d88a08d73da Mon Sep 17 00:00:00 2001 From: Serhii Khoma Date: Wed, 13 May 2020 22:30:15 +0300 Subject: [PATCH] refactor: add various comments throughout the code, change argument/type names to more descriptive, add explicit types --- GUIDE.md | 6 +- 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 +++++++------ 7 files changed, 267 insertions(+), 119 deletions(-) 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/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