Core

This module is for core types and logic.

Html Type

data Html m a
  = TextNode Text
  | Node Text [(Text, Prop m a)] [Html m a]
  | Potato (JSM RawNode)
  • m is an effect to run your event handlers.

  • a is what your event handlers produce.

These are only relevant to event handling. If your view has no event listeners, then m and a can be completely parametric and unconstrained.

Not Virtual DOM
Html will NOT be diffed, instead they will be marshalled into a type for a given backend. This separation allows the same code to run on any backend.

This structure is the DOM Tree. There are Nodes which contain lists of Html, and which is a leaf where you can store Text, and Potato where we can store a component built outside of the Shpadoinkle view (for example wrapping a JavaScript component).

EndoIso

(Html m) is a Functor, but not Prelude.Functor. It is not functorial in Hask but rather in EndoIso, where the objects are types and the morphisms are EndoIsos.

data EndoIso a b = EndoIso (a -> a) (a -> b) (b -> a)

Prop Type

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 Text should match the property key in standard JavaScript. For example, you can see the following are morally equivalent:

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:

h "div" [("className",  textProp "foo")] []

Listeners

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.

Continuation

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)

Backend Class

This is an interface for renders of Html:

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 Html.

This packages does not come with a backend implementation, and an implementation is required to run the shpadoinkle function.

Monad Transformer

b is expected to be a Monad Transformer, though this is not required; in practice, (b m) must have in instance of MonadJSM.

VNode

This type family points maps to the underlying representation native to the backend:

type VNode b m

In the case of binding to a JavaScript library, this would most likely be a newtype of JSVal. When binding to a typed implementation, this should just be set to the library type.

Interpret

This function describes how to marshal between Html and the native representation (i.e. VNode):

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.
2 The Html Shpadoinkle view that needs to be marshalled to the native representation for this backend.
3 A Monadic action that generates VNode.

Patch

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. RawNode is a newtype of JSVal.
2 The previously rendered VNode. On the first rendering of the application, this will be Nothing.
3 The VNode the user would like to render.
4 A Monadic action that actually renders in the browser and returns a new VNode. The returned (v :: VNode) will be (Just v) for 2 in the next render.

Setup

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 JSM (), then nothing will happen.

In the case of JavaScript-based backends, it will likely include steps like adding the library to the <head> of the page, or instantiating a JavaScript class.

The TVar

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 TVar 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.

Shpadoinkle

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 ()
1 Run the setup for the backend.
2 Get the DOM Node on which to append the view.
3 Pass the initial model to the view function, then convert the Html m to VNode b m.
4 Render the initial VNode b m.
5 Set up go to run whenever shouldUpdate. go renders subsequent states.

Everything else is built on top of this to simplify different setups.