This module is for core types and logic.
data Html m a = TextNode Text | Node Text [(Text, Prop m a)] [Html m a] | Potato (JSM RawNode)
mis an effect to run your event handlers.
ais what your event handlers produce.
These are only relevant to event handling. If your view has no event listeners, then
a can be completely parametric and unconstrained.
Not Virtual DOM
This structure is the DOM Tree. There are Nodes which contain lists of Html, and which is a leaf where you can store
Nodes (and only Nodes) in Html can have properties. These properties are represented as the following type, paired with
Text giving the property a field key:
data Prop m a = PText Text | PListener (RawNode -> RawEvent -> JSM a) | PFlag Bool
As you will recall,
Html has a constructor:
Node :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a
Where a list of this pair is passed. The
const div = document.createElement("div"); div.className = "foo";
Node "div" [("className", PText "foo")] 
Do Not use constructors directly!
It is recommended you do not use these constructors, but rather use the exported named functions:
The listener constructor is
PListener, which has the following type:
PListener :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a
The raw listener will always receive the
RawNode, which is the target of the event; and the
RawEvent, which is the event object itself. Both of these
newtypes are JSVal. This is needed so that you can still do low-level work; in practice it is expected you would use functions that allow you to ignore these raw components.
The type of a state update in Shpadoinkle. A Continuation builds up an atomic state update incrementally in a series of stages. For each stage we perform a monadic I/O computation and we can get a pure state updating function. When all of the stages have been executed we are left with a composition of the resulting pure state updating functions, and this composition is applied atomically to the state.
Additionally, a Continuation stage can feature a Rollback action which cancels all state updates generated so far but allows for further state updates to be generated based on further monadic I/O computation.
The functions generating each stage of the Continuation are called with states which reflect the current state of the app, with all the pure state updating functions generated so far having been applied to it, so that each stage "sees" both the current state (even if it changed since the start of computing the Continuation) and the updates made so far, although those updates are not committed to the real state until the Continuation finishes and they are all done atomically together.
data Continuation m a = Continuation (a -> a, a -> m (Continuation m a)) | Rollback (Continuation m a) | Pure (a -> a)
This is an interface for renders of
class Backend b m a | b m -> a where type VNode b m interpret :: (m ~> JSM) -> Html (b m) a -> b m (VNode b m) patch :: RawNode -> Maybe (VNode b m) -> VNode b m -> b m (VNode b m) setup :: JSM () -> JSM ()
This interface lets you plug into various rendering systems. So long as you can provide implementations of these three functions, you can use
shpadoinkle to get an application out of
This packages does not come with a backend implementation, and an implementation is required to run the
This type family points maps to the underlying representation native to the backend:
type VNode b m
JSVal. When binding to a typed implementation, this should just be set to the library type.
This function describes how to marshal between
Html and the native representation (i.e.
interpret :: (m ~> JSM) (1) -> Html (b m) a (2) -> b m (VNode b m) (3)
The interpret function can be Monadic, as it is likely going to require IO to obtain the native representation.
|1||Interpret is provided with a mechanism for getting from the end user provided Monad to JSM directly.|
|3||A Monadic action that generates
This function describes how updates are handled:
patch :: RawNode (1) -> Maybe (VNode b m) (2) -> VNode b m (3) -> b m (VNode b m) (4)
The interpret function can be Monadic, as it is likely going to require IO to apply the new
VNode to the view.
|1||This is the parent DOM Node that contains the application.
|2||The previously rendered
|4||A Monadic action that actually renders in the browser and returns a new
This is an optional IO action to perform any initial setup steps a given backend might require:
setup :: JSM () (1) -> JSM ()
|1||This is a callback you are responsible for executing after the setup process is complete. The callback is the entire application. If you do not evaluate the
The interface for driving the view is STM.
The Haskell ecosystem has many options for concurrent data structures. Many of these containers can be marshalled to the humble
Theoretically, you could write instances for containers such as IORef, Event t, and Auto m
The TVar is part of ensuring Shpadoinkle applications compose with one another as well as surrounding code. Consider a scenario where there is an existing piece of code that taps into a data stream and logs it:
territory <- newTVarIO mempty (1) _ <- forkIO . runConduit (2) $ readLogFile .| takeC 200 .| mapMC (\s -> atomically $ modifyTVar territory $ currentLog .~ s) (3) .| mapM_C processFurther shpadoinkle id runSnabbdom territory mempty view getBody (4)
|1||Create a TVar of the frontend model.|
|2||Some existing code uses Conduit to read a log file.|
|3||Now, to show each Log as it passes through, simply write it to the TVar, setting it with a Lens.|
|4||Start the application. Changes to the territory will be reflected in the view.|
This makes integrating the frontend state machine into existing work fairly easy, because often existing locations in the code can be used to update the
TVar. You can also listen for state changes originating from inside the Shpadoinkle application using existing machinery such as
retry from STM.
There is one application primitive, the
shpadoinkle function. It is where these different components come together and describes how they interrelate:
shpadoinkle :: forall b m a. Backend b m a => Monad (b m) => Eq a => (m ~> JSM) -> (TVar a -> b m ~> m) -> a -> TVar a -> (a -> Html (b m) a) -> b m RawNode -> JSM () shpadoinkle toJSM toM initial model view stage = do let j :: b m ~> JSM j = toJSM . toM model go :: RawNode -> VNode b m -> a -> JSM (VNode b m) go c n a = j $ do !m <- interpret toJSM (view a) patch c (Just n) m setup @b @m @a $ do (1) (c,n) <- j $ do c <- stage (2) n <- interpret toJSM (view initial) (3) _ <- patch c Nothing n (4) return (c,n) _ <- shouldUpdate (go c) n model (5) return ()
|2||Get the DOM Node on which to append the view.|
|3||Pass the initial model to the view function, then convert the
|4||Render the initial
Everything else is built on top of this to simplify different setups.