Composing
Composing HTML elements is straightforward in Shpadoinkle when the elements all have the same model type; nesting them in each other works automatically.
For an element to be a child of another element, they must have the same model type. This means that all elements in the tree under an Html m Model
(children, children of children, etc.) must have the type Html m Model
. Therefore, before an element can become the child of a parent, it must be transformed to have a matching model type, if the model types do not already match.
Changing the model type of an HTML element can be accomplished using the xC
functions which make use of the Continuous
typeclass. This typeclass is instantiated by types where the values can be changed by functions which change continuations.
Continuations are a concept in Shpadoinkle’s event handling system. Each event handler causes a continuation to be run. That continuation may perform I/O actions and/or update the model. If a continuation updates the model, then it does so atomically, which means that it is safe to have multiple continuations running concurrently. To learn more about continuations, see the Shpadoinkle-continuations package. |
The model type of an Html
element determines what type the event handlers expect the model to have. An event handler yields a continuation which may update the model. In order to change the model type of an Html
element, it is sufficient to change the type which the continuations operate on. As such, Html
instantiates Continuous
; any function which changes the type which continuations operate on can be used to change the model type of an Html
element.
First consider the case where you have an element x :: Html m a
and you need to change its type to Html m (a,b)
so that the event handlers act on the left side of the tuple. In this case you should use the leftC
function: leftC x :: Html m (a,b)
.
Similarly, if you have an element x :: Html m b
and you need to change its type Html m (a,b)
, then you should use the rightC
function: rightC x :: Html (a,b)
.
If you have an element x :: Html m ()
then you can freely vary its model type using voidC
: voidC x :: Html m a
. The event handlers will have the same side effects and they will not touch the model.
You can change the model type from anything to anything using the forgetC
function, which will strip out all event handlers, replacing them with no-ops. For any types a
, b
and monads m
, n
, if x :: Html m a
then forgetC x :: Html n b
.
Given a lens from a type b
to a type a
, consisting of a function set :: (a -> b -> b)
and a function get :: b -> a
, you can change an element x :: Html m a
to an element liftC set get x :: Html m b
using the liftC
function.
You can change an element x :: Html m a
to an element maybeC x :: Html m (Maybe a)
, where the event handlers are lifted into the Maybe
monad using fmap
(so they will have the same effect when the model is Just
and they will not affect the model when the model is Nothing
).
Given an isomorphism, you can change the type of an element from one side of the isomorphism to the other side. An isomorphism from a type a
to a type b
consists of a function f :: a -> b
and a function g :: b -> a
such that f . g = id
and g . f = id
. You can change an element x :: Html m a
in the following way: pimap (piiso f g) x :: Html m b
.
There is one more way of changing the model type of an element which you are likely to need, which is lifting into coproducts. Let’s say that you have two views in your app, represented by models of types a
and b
respectively. Then you can have an overall model of type Either a b
. How can you combine elements x :: Html m a
and y :: Html m b
into the same structure?
In this scenario it will make more sense to think of view functions as opposed to elements. By a view function, I mean a function which goes from the value of the model to an element. So let’s suppose we have view functions v :: a -> Html m a
and u :: b -> Html m b
. Then we can combine them as eitherH v u :: Either a b -> Html m (Either a b)
. This combined element will show the view v
when the model is a Left
and the view u
when the model is a Right
.
If you wish to have multiple views combined in this way but you do not want your model type to be an Either
type, then you can combine this coproduct pattern with an isomorphism. Suppose that f :: Either a b -> Model
and g :: Model -> Either a b
constitute an isomorphism, and v
and u
are as above. Then: pimap (piiso f g) . eitherH v u . g :: Model -> Html m Model
.
For examples of many of these patterns, see the Servant CRUD example.