data Html m a = TextNode Text | Node Text [(Text, Prop m a)] [Html m a] | Potato (JSM (RawNode, STM (Continuation m a)))
This package is for core types and logic.
mis an effect (i.e., a monad) to run your event handlers.
ais what your event handlers produce. It is the type of the view model.
These are only relevant to event handling. If your view has no event listeners, then
a can be completely parametric and unconstrained.
In reality, this type is Church encoded for performance reasons. The definition given above is not the actual definition. By applying the Church encoding transformation to the definition given above, we get the actual definition of
Not Virtual DOM
Nodes (and only Nodes) in Html can have properties. These properties are represented as the Church encoding of the following type, paired with the
Text property name:
data Prop m a = PText Text | PListener (RawNode -> RawEvent -> JSM (STM (Continuation m a))) | PFlag Bool
As you may recall,
Html has a constructor:
Node :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a
Node takes a list of properties:
[(Text, Prop m a)]. 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, e.g.:
The reason for this guidance is that we reserve the right to change the constructors of
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 be 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.
Also, a Continuation stage can be a Merge action which applies all state updates generated so far (in an atomic transaction) but allows for further state updates to be generated based on further monadic I/O computation. Effectively this allows multiple continuations to be strung together one after another as a single continuation with the same result as if the two continuations were run separately.
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. The updates generated by the continuation are not committed to the real state until the Continuation finishes or reaches a Merge action, at which point the updates are all done in an atomic transaction.
data Continuation m a = Continuation (a -> a) (a -> m (Continuation m a)) | Rollback (Continuation m a) | Merge (Continuation m a) | Pure (a -> a)
Do not use the constructors directly!
It is recommended that you do not use the constructors for Continuation. Instead you can use the functions provided to construct continuations, such as
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 software transactional memory (STM).
The Haskell ecosystem has many options for thread safe 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. Here is an example of how to listen for changes to a
do current <- readTVarIO model next <- atomically $ do current' <- readTVar model if current' == current then retry else return current' -- do what should happen when model changes; next contains the new value of model doSomething next
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) -> TVar a -> (a -> Html (b m) a) -> b m RawNode -> JSM () shpadoinkle toJSM toM model view stage = setup @b @m @a $ do (1) c <- j stage (2) initial <- readTVarIO model n <- go c Nothing initial (3) () <$ shouldUpdate (go c . Just) n model (4) where j :: b m ~> JSM j = toJSM . toM model go :: RawNode -> Maybe (VNode b m) -> a -> JSM (VNode b m) go c n a = j $ patch c n =<< interpret toJSM (view a)
|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.