From 51d08ce3c5e1ebd845f5443743e0fd986ba2558c Mon Sep 17 00:00:00 2001 From: Fyodor Soikin Date: Mon, 29 Jul 2024 10:56:27 -0400 Subject: [PATCH] Make ReactElement a proper Monoid and other ergonomic tweaks (#84) --- CHANGELOG.md | 13 +++++++ docs/react-ffi.md | 62 +++++++++++++++++++++++++++++--- packages.dhall | 2 +- src/Elmish/Component.purs | 20 +++++------ src/Elmish/Foreign.purs | 2 +- src/Elmish/React.js | 17 +++++++++ src/Elmish/React.purs | 65 +++++++++++++++++++--------------- src/Elmish/React/DOM.js | 2 -- src/Elmish/React/DOM.purs | 22 ------------ src/Elmish/React/Import.purs | 6 ++-- src/Elmish/React/Internal.js | 2 ++ src/Elmish/React/Internal.purs | 25 +++++++++++++ src/Elmish/React/Ref.purs | 6 ++++ src/Elmish/Trace.js | 10 ------ src/Elmish/Trace.purs | 7 ---- test/Foreign.purs | 29 ++++++++++++++- test/Main.purs | 2 ++ test/ReactElement.js | 9 +++++ test/ReactElement.purs | 56 +++++++++++++++++++++++++++++ 19 files changed, 268 insertions(+), 89 deletions(-) delete mode 100644 src/Elmish/React/DOM.js delete mode 100644 src/Elmish/React/DOM.purs create mode 100644 src/Elmish/React/Internal.js create mode 100644 src/Elmish/React/Internal.purs delete mode 100644 src/Elmish/Trace.js delete mode 100644 src/Elmish/Trace.purs create mode 100644 test/ReactElement.js create mode 100644 test/ReactElement.purs diff --git a/CHANGELOG.md b/CHANGELOG.md index d42c040..10efa28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.13.0 + +### Changed +* `ReactElement` is now a full `Monoid` with `empty` as identity and append + creating React.Fragment elements. +* **Breaking**: module `Elmish.React.DOM` has been removed and its contents + moved to `Elmish.React`. +* **Breaking**: module `Elmish.Trace` has been removed. Its sole export has been + part of the standard `debug` library for a while now. +* Fixed a bug with `readForeign` and nested `Nullable`s: reading `[1,"foo",2]` + as `Nullable (Array Int)` would complain that the second element is bogus + (which is true) and incorrectly state that the expected type was `Nullable Int`. + ## 0.12.0 ### Changed diff --git a/docs/react-ffi.md b/docs/react-ffi.md index a8a0e34..c8f6535 100644 --- a/docs/react-ffi.md +++ b/docs/react-ffi.md @@ -6,9 +6,63 @@ nav_order: 6 # Using React components {:.no_toc} -> **Under construction**. This page is unfinished. Many headings just have some -> bullet points sketching the main points that should be discussed. +Because Elmish is just a thin layer on top of React, it is quite easy to use +non-PureScript React components from the wider ecosystem. -1. TOC -{:toc} +A typical import of a React component consists of three parts: +* A row of props, with optional props denoted via `Opt`. +* Actual FFI-import of the component constructor. This import is weakly + typed and shouldn't be exported from the module. Consider it internal + implementation detail. +* Strongly-typed, PureScript-friendly function that constructs the + component. The body of such function usually consists of just a call + to `createElement` (or `createElement'` for childless components), its + only purpose being the type signature. This function is what should be + exported for use by consumers. + +Classes and type aliases provided in this module, when applied to the +constructor function, make it possible to pass only partial props to it, +while still ensuring their correct types and presence of non-optional ones. +This is facilitated by the +[undefined-is-not-a-problem](https://github.com/paluh/purescript-undefined-is-not-a-problem/) library. + +## Example + +### The JSX file with component implementation +```jsx +// `world` prop is required, `hello` and `highlight` are optional +export const MyComponent = ({ hello, world, highlight }) => +
+ {hello || "Hello"}, + {world} +
+``` + +### PureScript FFI module +```haskell +module MyComponent(Props, myComponent) where + +import Data.Undefined.NoProblem (Opt) +import Elmish.React (createElement) +import Elmish.React.Import (ImportedReactComponentConstructor, ImportedReactComponent) + +type Props = ( world :: String, hello :: Opt String, highlight :: Opt Boolean ) + +myComponent :: ImportedReactComponentConstructor Props +myComponent = createElement myComponent_ + +foreign import myComponent_ :: ImportedReactComponent +``` + +### PureScript use site +```haskell +import MyComponent (myComponent) +import Elmish.React (fragment) as H + +view :: ... +view = H.fragment + [ myComponent { world: "world" } + , myComponent { hello: "Goodbye", world: "cruel world!", highlight: true } + ] +``` diff --git a/packages.dhall b/packages.dhall index c5f301f..ad17a4a 100644 --- a/packages.dhall +++ b/packages.dhall @@ -8,5 +8,5 @@ in upstream with elmish-html = { dependencies = [ "prelude", "record" ] , repo = "https://github.com/collegevine/purescript-elmish-html.git" - , version = "v0.8.2" + , version = "tweaks" } diff --git a/src/Elmish/Component.purs b/src/Elmish/Component.purs index 8d4016a..44a47ca 100644 --- a/src/Elmish/Component.purs +++ b/src/Elmish/Component.purs @@ -28,9 +28,9 @@ import Effect (Effect, foreachE) import Effect.Aff (Aff, Milliseconds(..), delay, launchAff_) import Effect.Class (class MonadEffect, liftEffect) import Elmish.Dispatch (Dispatch) -import Elmish.React (ReactComponent, ReactComponentInstance, ReactElement, getField, setField) +import Elmish.React (ReactComponent, ReactComponentInstance, ReactElement) +import Elmish.React.Internal (Field(..), getField, setField) import Elmish.State (StateStrategy, dedicatedStorage, localState) -import Elmish.Trace (traceTime) -- | A UI component state transition: wraps the new state value together with a -- | (possibly empty) list of effects that the transition has caused (called @@ -243,10 +243,10 @@ withTrace :: ∀ m msg state withTrace def = def { update = tracingUpdate, view = tracingView } where tracingUpdate s m = - let (Transition s cmds) = traceTime "Update" \_ -> def.update s $ Debug.spy "Message" m + let (Transition s cmds) = Debug.traceTime "Update" \_ -> def.update s $ Debug.spy "Message" m in Transition (Debug.spy "State" s) cmds tracingView s d = - traceTime "Rendering" \_ -> def.view s d + Debug.traceTime "Rendering" \_ -> def.view s d -- | This function is low level, not intended for a use in typical consumer -- | code. Use `construct` or `wrapWithLocalState` instead. @@ -307,13 +307,13 @@ bindComponent cmpt stateStrategy = \def -> -- Explicit lambda to make sure `def` sequence_ =<< getSubscriptions component setSubscriptions [] component - subscriptionsField = "__subscriptions" - getSubscriptions = getField @(Array (Effect Unit)) subscriptionsField >>> map (fromMaybe []) - setSubscriptions = setField @(Array (Effect Unit)) subscriptionsField + subscriptionsField = Field @"__subscriptions" @(Array (Effect Unit)) + getSubscriptions = getField subscriptionsField >>> map (fromMaybe []) + setSubscriptions = setField subscriptionsField - unmountedField = "__unmounted" - getUnmounted = getField @Boolean unmountedField >>> map (fromMaybe false) - setUnmounted = setField @Boolean unmountedField + unmountedField = Field @"__unmounted" @Boolean + getUnmounted = getField unmountedField >>> map (fromMaybe false) + setUnmounted = setField unmountedField -- | Given a `ComponentDef'`, binds that def to a freshly created React class, -- | instantiates that class, and returns a rendering function. diff --git a/src/Elmish/Foreign.purs b/src/Elmish/Foreign.purs index 7f85bca..c2fd889 100644 --- a/src/Elmish/Foreign.purs +++ b/src/Elmish/Foreign.purs @@ -195,7 +195,7 @@ instance CanReceiveFromJavaScript a => CanReceiveFromJavaScript (Nullable a) whe | otherwise = case validateForeignType @a v of Valid -> Valid - Invalid err -> Invalid err { expected = "Nullable " <> err.expected } + Invalid err -> Invalid err { expected = if err.path == "" then "Nullable " <> err.expected else err.expected } instance CanPassToJavaScript a => CanPassToJavaScript (Opt a) instance CanReceiveFromJavaScript a => CanReceiveFromJavaScript (Opt a) where diff --git a/src/Elmish/React.js b/src/Elmish/React.js index f471e0e..63d5634 100644 --- a/src/Elmish/React.js +++ b/src/Elmish/React.js @@ -19,6 +19,23 @@ export var hydrate_ = ReactDOM.hydrate; export var renderToString = (ReactDOMServer && ReactDOMServer.renderToString) || (_ => ""); export var unmount_ = ReactDOM.unmountComponentAtNode +export var fragment_ = React.Fragment; + +export var appendElement_ = a => b => { + const childrenOf = x => { + if (x === false || x === null || typeof x === 'undefined') return [] + if (x.type === React.Fragment) { + const children = x.props?.children + if (children instanceof Array) return children + if (children === false || children === null || typeof children === 'undefined') return [] + return [children] + } + return [x] + } + const allChildren = [...childrenOf(a), ...childrenOf(b)] + return allChildren.length === 0 ? false : React.createElement(React.Fragment, null, allChildren) +} + export function createElement_(component, props, children) { // The type of `children` is `Array ReactElement`. If we pass that in as // third parameter of `React.createElement` directly, React complains about diff --git a/src/Elmish/React.purs b/src/Elmish/React.purs index cf562d1..2e56a1c 100644 --- a/src/Elmish/React.purs +++ b/src/Elmish/React.purs @@ -1,31 +1,31 @@ module Elmish.React - ( ReactElement - , ReactComponent - , ReactComponentInstance - , class ValidReactProps - , class ReactChildren, asReactChildren - , assignState - , createElement - , createElement' - , getField - , getState - , hydrate - , setField - , setState - , render - , renderToString - , unmount - , module Ref - ) where + ( ReactElement + , ReactComponent + , ReactComponentInstance + , class ValidReactProps + , class ReactChildren, asReactChildren + , assignState + , createElement + , createElement' + , empty + , fragment + , getState + , hydrate + , setState + , render + , renderToString + , text + , unmount + , module Ref + ) where import Prelude import Data.Function.Uncurried (Fn3, runFn3) -import Data.Maybe (Maybe) import Data.Nullable (Nullable) import Effect (Effect) import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, runEffectFn1, runEffectFn2, runEffectFn3) -import Elmish.Foreign (class CanPassToJavaScript, class CanReceiveFromJavaScript, Foreign, readForeign) +import Elmish.Foreign (class CanPassToJavaScript) import Elmish.React.Ref (Ref, callbackRef) as Ref import Prim.TypeError (Text, class Fail) import Unsafe.Coerce (unsafeCoerce) @@ -34,6 +34,9 @@ import Web.DOM as HTML -- | Instantiated subtree of React DOM. JSX syntax produces values of this type. foreign import data ReactElement :: Type +instance Semigroup ReactElement where append = appendElement_ +instance Monoid ReactElement where mempty = empty + -- | This type represents constructor of a React component with a particular -- | behavior. The type prameter is the record of props (in React lingo) that -- | this component expects. Such constructors can be "rendered" into @@ -82,6 +85,20 @@ createElement' :: ∀ props -> ReactElement createElement' component props = createElement component props ([] :: Array ReactElement) +-- | Empty React element. +empty :: ReactElement +empty = unsafeCoerce false + +-- | Render a plain string as a React element. +text :: String -> ReactElement +text = unsafeCoerce + +-- | Wraps multiple React elements as a single one (import of React.Fragment) +fragment :: Array ReactElement -> ReactElement +fragment = createElement fragment_ {} + +foreign import fragment_ :: ReactComponent {} +foreign import appendElement_ :: ReactElement -> ReactElement -> ReactElement -- | Asserts that the given type is a valid React props structure. Currently -- | there are three rules for what is considered "valid": @@ -112,14 +129,6 @@ setState :: ∀ state. ReactComponentInstance -> state -> (Effect Unit) -> Effec setState = runEffectFn3 setState_ foreign import setState_ :: ∀ state. EffectFn3 ReactComponentInstance state (Effect Unit) Unit -getField :: ∀ @a. CanReceiveFromJavaScript a => String -> ReactComponentInstance -> Effect (Maybe a) -getField field object = runEffectFn2 getField_ field object <#> readForeign @a -foreign import getField_ :: EffectFn2 String ReactComponentInstance Foreign - -setField :: ∀ @a. CanPassToJavaScript a => String -> a -> ReactComponentInstance -> Effect Unit -setField = runEffectFn3 setField_ -foreign import setField_ :: ∀ a. EffectFn3 String a ReactComponentInstance Unit - -- | The equivalent of `this.state = x`, as opposed to `setState`, which is the -- | equivalent of `this.setState(x)`. This function is used in a component's -- | constructor to set the initial state. diff --git a/src/Elmish/React/DOM.js b/src/Elmish/React/DOM.js deleted file mode 100644 index a593f74..0000000 --- a/src/Elmish/React/DOM.js +++ /dev/null @@ -1,2 +0,0 @@ -import { Fragment } from "react/index.js"; -export var fragment_ = Fragment; diff --git a/src/Elmish/React/DOM.purs b/src/Elmish/React/DOM.purs deleted file mode 100644 index e310bca..0000000 --- a/src/Elmish/React/DOM.purs +++ /dev/null @@ -1,22 +0,0 @@ -module Elmish.React.DOM - ( empty - , text - , fragment - ) where - -import Elmish.React (ReactComponent, ReactElement, createElement) -import Unsafe.Coerce (unsafeCoerce) - --- | Empty React element. -empty :: ReactElement -empty = unsafeCoerce false - --- | Render a plain string as a React element. -text :: String -> ReactElement -text = unsafeCoerce - --- | Wraps multiple React elements as a single one (import of React.Fragment) -fragment :: Array ReactElement -> ReactElement -fragment = createElement fragment_ {} - -foreign import fragment_ :: ReactComponent {} diff --git a/src/Elmish/React/Import.purs b/src/Elmish/React/Import.purs index 2aa88c5..d702cb6 100644 --- a/src/Elmish/React/Import.purs +++ b/src/Elmish/React/Import.purs @@ -30,7 +30,7 @@ -- | -- | -- | -- PureScript --- | module MyComponent(Props, OptProps, myComponent) where +-- | module MyComponent(Props, myComponent) where -- | -- | import Data.Undefined.NoProblem (Opt) -- | import Elmish.React (createElement) @@ -38,7 +38,7 @@ -- | -- | type Props = ( world :: String, hello :: Opt String, highlight :: Opt Boolean ) -- | --- | myComponent :: ImportedReactComponentConstructor Props OptProps +-- | myComponent :: ImportedReactComponentConstructor Props -- | myComponent = createElement myComponent_ -- | -- | foreign import myComponent_ :: ImportedReactComponent @@ -46,7 +46,7 @@ -- | -- | -- PureScript use site -- | import MyComponent (myComponent) --- | import Elmish.React.DOM (fragment) +-- | import Elmish.React (fragment) as H -- | -- | view :: ... -- | view = H.fragment diff --git a/src/Elmish/React/Internal.js b/src/Elmish/React/Internal.js new file mode 100644 index 0000000..206b5b7 --- /dev/null +++ b/src/Elmish/React/Internal.js @@ -0,0 +1,2 @@ +export const getField_ = (field, obj) => obj[field] +export const setField_ = (field, value, obj) => obj[field] = value diff --git a/src/Elmish/React/Internal.purs b/src/Elmish/React/Internal.purs new file mode 100644 index 0000000..8ee8d7b --- /dev/null +++ b/src/Elmish/React/Internal.purs @@ -0,0 +1,25 @@ +module Elmish.React.Internal + ( Field(..) + , getField + , setField + ) where + +import Prelude + +import Data.Maybe (Maybe) +import Data.Symbol (class IsSymbol, reflectSymbol) +import Effect (Effect) +import Effect.Uncurried (EffectFn2, EffectFn3, runEffectFn2, runEffectFn3) +import Elmish.Foreign (class CanPassToJavaScript, class CanReceiveFromJavaScript, Foreign, readForeign) +import Elmish.React (ReactComponentInstance) +import Type.Proxy (Proxy(..)) + +data Field (f :: Symbol) (a :: Type) = Field + +getField :: ∀ f a. CanReceiveFromJavaScript a => IsSymbol f => Field f a -> ReactComponentInstance -> Effect (Maybe a) +getField _ object = runEffectFn2 getField_ (reflectSymbol $ Proxy @f) object <#> readForeign @a +foreign import getField_ :: EffectFn2 String ReactComponentInstance Foreign + +setField :: ∀ f a. CanPassToJavaScript a => IsSymbol f => Field f a -> a -> ReactComponentInstance -> Effect Unit +setField _ = runEffectFn3 setField_ $ reflectSymbol $ Proxy @f +foreign import setField_ :: ∀ a. EffectFn3 String a ReactComponentInstance Unit diff --git a/src/Elmish/React/Ref.purs b/src/Elmish/React/Ref.purs index c36247f..2b7c41e 100644 --- a/src/Elmish/React/Ref.purs +++ b/src/Elmish/React/Ref.purs @@ -31,7 +31,13 @@ instance CanPassToJavaScript (Ref a) -- | view :: State -> Dispatch Message -> ReactElement -- | view state dispatch = -- | H.input_ "" { ref: callbackRef state.inputElement (dispatch <<< RefChanged), … } +-- | +-- | update :: State -> Message -> Transition Message State +-- | update state = case _ of +-- | RefChanged ref -> pure state { inputElement = ref } +-- | … -- | ``` +-- | callbackRef :: forall el. Maybe el -> (Maybe el -> Effect Unit) -> Ref el callbackRef ref setRef = mkCallbackRef $ mkEffectFn1 \ref' -> case ref, Nullable.toMaybe ref' of Nothing, Nothing -> pure unit diff --git a/src/Elmish/Trace.js b/src/Elmish/Trace.js deleted file mode 100644 index d7462f1..0000000 --- a/src/Elmish/Trace.js +++ /dev/null @@ -1,10 +0,0 @@ -export function traceTime(name) { - return function(f) { - const start = new Date() - const res = f() - const end = new Date() - console.log(name + " took " + (end - start) + "ms") - return res - } -} - \ No newline at end of file diff --git a/src/Elmish/Trace.purs b/src/Elmish/Trace.purs deleted file mode 100644 index 6089c37..0000000 --- a/src/Elmish/Trace.purs +++ /dev/null @@ -1,7 +0,0 @@ -module Elmish.Trace - ( traceTime - ) where - -import Prelude - -foreign import traceTime :: forall a. String -> (Unit -> a) -> a diff --git a/test/Foreign.purs b/test/Foreign.purs index 4e3336d..cf90bbe 100644 --- a/test/Foreign.purs +++ b/test/Foreign.purs @@ -5,7 +5,7 @@ import Prelude import Data.Array ((!!)) import Data.Either (Either(..)) import Data.Maybe (Maybe(..)) -import Data.Nullable (Nullable, notNull, null) +import Data.Nullable (Nullable, notNull, null, toMaybe) import Elmish.Foreign (class CanReceiveFromJavaScript, readForeign, readForeign') import Foreign (unsafeToForeign) import Foreign.Object (Object) @@ -56,6 +56,19 @@ spec = describe "Elmish.Foreign" do (read @{ x :: Nullable { y :: Int } } { x: null :: _ Int }) `shouldEqual` Just { x: null } + (read' @(Nullable (Array Int)) null <#> toMaybe) `shouldEqual` Right Nothing + (read' @(Array (Nullable Int)) [f 42, f 5, f null] <#> map toMaybe) `shouldEqual` Right [Just 42, Just 5, Nothing] + + (read' @(Nullable { x :: Int }) null <#> toMaybe) `shouldEqual` Right Nothing + + let r = read' @(Nullable { x :: Int, y :: Nullable { z :: Int } }) { x: 42, y: null } + (r <#> toMaybe <#> map _.x) `shouldEqual` Right (Just 42) + (r <#> toMaybe <#> map _.y <#> map toMaybe) `shouldEqual` Right (Just Nothing) + + let q = read @{ foo :: String, one :: Nullable Int } { foo: "bar", one: null } + (q <#> _.foo) `shouldEqual` Just "bar" + (q <#> _.one <#> toMaybe) `shouldEqual` Just Nothing + it "treats missing record fields as null" do read { x: "foo" } `shouldEqual` Just { x: "foo", y: null :: _ Int } @@ -70,6 +83,20 @@ spec = describe "Elmish.Foreign" do it "nullable" do (read' @(Nullable Int) "foo") `shouldEqual` Left "Expected Nullable Int but got: \"foo\"" + it "nullable array" do + (read' @(Nullable (Array Int)) [f 42, f "foo"]) `shouldEqual` Left "[1]: expected Int but got: \"foo\"" + (read' @(Nullable (Array Int)) [f 42, f 5, f "foo"]) `shouldEqual` Left "[2]: expected Int but got: \"foo\"" + + it "nullable record" do + (read' @(Nullable { x :: Int, y :: Boolean }) { x: 42, y: "foo" }) + `shouldEqual` Left ".y: expected Boolean but got: \"foo\"" + + (read' @(Nullable { x :: Int, y :: { z :: Int } }) { x: 42, y: "foo" }) + `shouldEqual` Left ".y: expected Object but got: \"foo\"" + + (read' @(Nullable { x :: Int, y :: { z :: Nullable Int } }) { x: 42, y: null }) + `shouldEqual` Left ".y: expected Object but got: " + it "nested within array" do (read' @(Array Int) [f 42, f "foo"]) `shouldEqual` Left "[1]: expected Int but got: \"foo\"" (read' @(Array Int) [f 42, f 5, f "foo"]) `shouldEqual` Left "[2]: expected Int but got: \"foo\"" diff --git a/test/Main.purs b/test/Main.purs index ac96583..c4c97c0 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -7,6 +7,7 @@ import Effect.Aff (launchAff_) import Test.Component as Component import Test.Foreign as Foreign import Test.LocalState as LocalState +import Test.ReactElement as ReactElement import Test.Spec.Reporter (specReporter) import Test.Spec.Runner (runSpec) import Test.Subscriptions as Subscriptions @@ -17,3 +18,4 @@ main = launchAff_ $ runSpec [specReporter] do Component.spec LocalState.spec Subscriptions.spec + ReactElement.spec diff --git a/test/ReactElement.js b/test/ReactElement.js new file mode 100644 index 0000000..b2afcdc --- /dev/null +++ b/test/ReactElement.js @@ -0,0 +1,9 @@ +import React from 'react' + +export const isFragment = e => e.type === React.Fragment +export const elementChildren = e => e.props.children +export const elementType = e => typeof e === 'string' ? 'text' : e.type +export const elementText = e => + typeof e === 'string' ? e : + typeof e.props.children === 'string' ? e.props.children : + e.props.children.map(elementText).join('|') diff --git a/test/ReactElement.purs b/test/ReactElement.purs new file mode 100644 index 0000000..9abf03c --- /dev/null +++ b/test/ReactElement.purs @@ -0,0 +1,56 @@ +module Test.ReactElement (spec) where + +import Prelude + +import Data.Array (fold) +import Elmish (ReactElement) +import Elmish.HTML.Styled as H +import Test.Spec (Spec, describe, it) +import Test.Spec.Assertions (shouldEqual) + +spec :: Spec Unit +spec = describe "Elmish.React.ReactElement" do + describe "Monoid instance" do + + it "should have `empty` as identity" do + (H.div "" "One" <> mempty) + `shouldBeFragmentOf` ["div:One"] + (mempty <> H.div "" "Two") + `shouldBeFragmentOf` ["div:Two"] + + it "should append two elements" do + (H.div "" "One" <> H.span "" "Two") + `shouldBeFragmentOf` ["div:One", "span:Two"] + + it "should append one fragment and one element" do + (H.fragment [ H.text "One", H.p "" "Two" ] <> H.text "Three") + `shouldBeFragmentOf` ["text:One", "p:Two", "text:Three"] + + it "should append one element and one fragment" do + (H.a "" "One" <> H.fragment [ H.text "Two", H.text "Three" ]) + `shouldBeFragmentOf` ["a:One", "text:Two", "text:Three"] + + it "should append two fragments" do + (H.fragment [ H.b "" "One", H.div "" "Two" ] <> H.fragment [ H.p "" "Three", H.text "Four" ]) + `shouldBeFragmentOf` ["b:One", "div:Two", "p:Three", "text:Four"] + + it "should not flatten nested elements" do + (H.div "" [H.text "One", H.text "Two"] <> H.p "" "Three" <> H.a "" [H.text "Four", H.p "" "Five"]) + `shouldBeFragmentOf` ["div:One|Two", "p:Three", "a:Four|Five"] + + it "should be foldable" do + (fold $ H.text <$> ["One", "Two", "Three"]) + `shouldBeFragmentOf` ["text:One", "text:Two", "text:Three"] + + where + shouldBeFragmentOf r expected = do + isFragment r `shouldEqual` true + showFragment r `shouldEqual` expected + + showFragment f = showElement <$> elementChildren f + showElement e = elementType e <> ":" <> elementText e + +foreign import elementChildren :: ReactElement -> Array ReactElement +foreign import elementType :: ReactElement -> String +foreign import elementText :: ReactElement -> String +foreign import isFragment :: ReactElement -> Boolean